Compare commits
	
		
			143 Commits
		
	
	
		
			0.1.5
			...
			56e6eb3609
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 56e6eb3609 | |||
| c49140e7f5 | |||
| 1276635a3e | |||
| f00dc50215 | |||
| 35de8a6395 | |||
| 96e155a49a | |||
| c02fb06eb2 | |||
| a9d5649bef | |||
| 650d61e95f | |||
| 63d854ffbe | |||
| a1c846be33 | |||
| 1b9f212e66 | |||
| 7805ddc270 | |||
| c1455bccad | |||
| dd956be93f | |||
| 04361a5838 | |||
| cb049cb178 | |||
| 5a41d8b3e7 | |||
| 64320dbdae | |||
| a03f8ebb8a | |||
| 4e17739b4d | |||
| d495db0bb3 | |||
| bf91eadd8e | |||
| 7d16cec5db | |||
| 50b7f8f9b6 | |||
| a0d3fb09f3 | |||
| 2d0f6eefab | |||
| d33da32b2f | |||
| 468489f872 | |||
| 5dedb4668b | |||
| 0d4deb6805 | |||
| 8f288afeb9 | |||
| aa5a1f30f9 | |||
| daa50039a7 | |||
| 98bb99881d | |||
| 56956f37fb | |||
| a0b93d0f46 | |||
| 066587f839 | |||
| 5cde99de90 | |||
| cc29f875fa | |||
| 5f2e69abbd | |||
| 8ef5147078 | |||
| c5e82c60b6 | |||
| c7debb331c | |||
| bc2c936480 | |||
| 9b4837a044 | |||
| bd5dd57c6c | |||
| 4f0f3ec610 | |||
| 6b7abdb3e1 | |||
| db2ec3b4a3 | |||
| f8a1d88e0e | |||
| 0eafb54266 | |||
| b1d442ea88 | |||
| 8b59339f74 | |||
| 563bbaedf8 | |||
| 12814be5e1 | |||
| bc034c0361 | |||
| eb9280fa7e | |||
| ad1e44fd42 | |||
| 482a3902b5 | |||
| 2fa1105f40 | |||
| df44be2b1b | |||
| 56dae6cdfe | |||
| 8ef3bd65a3 | |||
| 6e635d1b30 | |||
| ebe09bcf84 | |||
| 0ea953a34c | |||
| 1284a0c4b1 | |||
| fd0b2c455a | |||
| f1139d0ec4 | |||
| f10d061279 | |||
| cd6bb6fbc3 | |||
| ad7810a8ea | |||
| 5c05f3b6fe | |||
| cd7220cea3 | |||
| 30d3f76fe1 | |||
| ed8c20e967 | |||
| eb75937587 | |||
| 7b60938a75 | |||
| f19d024d8a | |||
| 1726a1b72e | |||
| 1fbed50b0f | |||
| f990fafc6a | |||
| 737484c36e | |||
| 2cb6aa8d33 | |||
| 24f608163f | |||
| 6dbadc58ae | |||
| 40f301ca71 | |||
| 178111ce80 | |||
| f155a43ee8 | |||
| 50c4878453 | |||
| bbaa1dfd6b | |||
| 2c2b9fefc9 | |||
| 51e6f49d17 | |||
| bafc824a32 | |||
| 8c2f7e0509 | |||
| 9289b79afa | |||
| 87f4d18a6d | |||
| 30bee92e37 | |||
| 22abc7a8d0 | |||
| f32d52db26 | |||
| d77421613c | |||
| 5a685ac00e | |||
| a5c12b3e17 | |||
| 27af5acf8b | |||
| 469428c007 | |||
| 0d61e5ee96 | |||
| 89413df8c4 | |||
| 8ccd94818c | |||
| 482f339da2 | |||
| af911f211c | |||
| 1cd28e43b8 | |||
| 695002c9d9 | |||
| dde60d71d1 | |||
| 91794ddc55 | |||
| 1e3cec95fe | |||
| f0a8c515f9 | |||
| e99775eef2 | |||
| 6760f83ca0 | |||
| 10877bb754 | |||
| 38a581b639 | |||
| 9408705430 | |||
| 302aa7eeda | |||
| fd4fdcc36b | |||
| 3cb723b9e4 | |||
| 1f92c452d1 | |||
| c10cff63ea | |||
| 70faed71d0 | |||
| b5c46c09ec | |||
| 7745765fc0 | |||
| e16fbba810 | |||
| e35bdf101b | |||
| aa314e5ca1 | |||
| 616de3c117 | |||
| 74bcec6cfe | |||
| 41090c46d0 | |||
| c3f5744ad6 | |||
| 95a339fe02 | |||
| 1e3b027367 | |||
| dd46e1795f | |||
| 9e517e6477 | |||
| 7813738f91 | |||
| c1337ebc10 | 
							
								
								
									
										14
									
								
								.credo.exs
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.credo.exs
									
									
									
									
									
								
							@@ -157,17 +157,17 @@
 | 
				
			|||||||
        #
 | 
					        #
 | 
				
			||||||
        # Controversial and experimental checks (opt-in, just replace `false` with `[]`)
 | 
					        # Controversial and experimental checks (opt-in, just replace `false` with `[]`)
 | 
				
			||||||
        #
 | 
					        #
 | 
				
			||||||
        {Credo.Check.Consistency.MultiAliasImportRequireUse, false},
 | 
					        {Credo.Check.Consistency.MultiAliasImportRequireUse, []},
 | 
				
			||||||
        {Credo.Check.Consistency.UnusedVariableNames, false},
 | 
					        {Credo.Check.Consistency.UnusedVariableNames, [force: :meaningful]},
 | 
				
			||||||
        {Credo.Check.Design.DuplicatedCode, false},
 | 
					        {Credo.Check.Design.DuplicatedCode, false},
 | 
				
			||||||
        {Credo.Check.Readability.AliasAs, false},
 | 
					        {Credo.Check.Readability.AliasAs, false},
 | 
				
			||||||
        {Credo.Check.Readability.BlockPipe, false},
 | 
					        {Credo.Check.Readability.BlockPipe, false},
 | 
				
			||||||
        {Credo.Check.Readability.ImplTrue, false},
 | 
					        {Credo.Check.Readability.ImplTrue, false},
 | 
				
			||||||
        {Credo.Check.Readability.MultiAlias, false},
 | 
					        {Credo.Check.Readability.MultiAlias, false},
 | 
				
			||||||
        {Credo.Check.Readability.SeparateAliasRequire, false},
 | 
					        {Credo.Check.Readability.SeparateAliasRequire, []},
 | 
				
			||||||
        {Credo.Check.Readability.SinglePipe, false},
 | 
					        {Credo.Check.Readability.SinglePipe, false},
 | 
				
			||||||
        {Credo.Check.Readability.Specs, false},
 | 
					        {Credo.Check.Readability.Specs, false},
 | 
				
			||||||
        {Credo.Check.Readability.StrictModuleLayout, false},
 | 
					        {Credo.Check.Readability.StrictModuleLayout, []},
 | 
				
			||||||
        {Credo.Check.Readability.WithCustomTaggedTuple, false},
 | 
					        {Credo.Check.Readability.WithCustomTaggedTuple, false},
 | 
				
			||||||
        {Credo.Check.Refactor.ABCSize, false},
 | 
					        {Credo.Check.Refactor.ABCSize, false},
 | 
				
			||||||
        {Credo.Check.Refactor.AppendSingleItem, false},
 | 
					        {Credo.Check.Refactor.AppendSingleItem, false},
 | 
				
			||||||
@@ -176,9 +176,9 @@
 | 
				
			|||||||
        {Credo.Check.Refactor.NegatedIsNil, false},
 | 
					        {Credo.Check.Refactor.NegatedIsNil, false},
 | 
				
			||||||
        {Credo.Check.Refactor.PipeChainStart, false},
 | 
					        {Credo.Check.Refactor.PipeChainStart, false},
 | 
				
			||||||
        {Credo.Check.Refactor.VariableRebinding, false},
 | 
					        {Credo.Check.Refactor.VariableRebinding, false},
 | 
				
			||||||
        {Credo.Check.Warning.LeakyEnvironment, false},
 | 
					        {Credo.Check.Warning.LeakyEnvironment, []},
 | 
				
			||||||
        {Credo.Check.Warning.MapGetUnsafePass, false},
 | 
					        {Credo.Check.Warning.MapGetUnsafePass, []},
 | 
				
			||||||
        {Credo.Check.Warning.UnsafeToAtom, false}
 | 
					        {Credo.Check.Warning.UnsafeToAtom, []}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        #
 | 
					        #
 | 
				
			||||||
        # Custom checks can be created using `mix credo.gen.check`.
 | 
					        # Custom checks can be created using `mix credo.gen.check`.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								.drone.yml
									
									
									
									
									
								
							@@ -13,20 +13,24 @@ steps:
 | 
				
			|||||||
    mount:
 | 
					    mount:
 | 
				
			||||||
      - _build
 | 
					      - _build
 | 
				
			||||||
      - deps
 | 
					      - deps
 | 
				
			||||||
      - assets/node_modules/
 | 
					      - .npm
 | 
				
			||||||
 | 
					      - .mix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: test
 | 
					- name: test
 | 
				
			||||||
  image: elixir:1.14.1-alpine
 | 
					  image: elixir:1.14.4-alpine
 | 
				
			||||||
  environment:
 | 
					  environment:
 | 
				
			||||||
    TEST_DATABASE_URL: ecto://postgres:postgres@database/memex_test
 | 
					    TEST_DATABASE_URL: ecto://postgres:postgres@database/memex_test
 | 
				
			||||||
    HOST: testing.example.tld
 | 
					    HOST: testing.example.tld
 | 
				
			||||||
 | 
					    MIX_HOME: /drone/src/.mix
 | 
				
			||||||
 | 
					    MIX_ARCHIVES: /drone/src/.mix/archives
 | 
				
			||||||
 | 
					    MIX_ENV: test
 | 
				
			||||||
  commands:
 | 
					  commands:
 | 
				
			||||||
  - apk add --no-cache build-base npm git python3
 | 
					  - apk add --no-cache build-base npm git
 | 
				
			||||||
  - mix local.rebar --force
 | 
					  - mix local.rebar --force --if-missing
 | 
				
			||||||
  - mix local.hex --force
 | 
					  - mix local.hex --force --if-missing
 | 
				
			||||||
  - mix deps.get
 | 
					  - mix deps.get
 | 
				
			||||||
  - mix deps.compile
 | 
					  - npm set cache .npm
 | 
				
			||||||
  - npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
 | 
					  - npm --prefix ./assets ci --no-audit --prefer-offline
 | 
				
			||||||
  - npm run --prefix ./assets deploy
 | 
					  - npm run --prefix ./assets deploy
 | 
				
			||||||
  - mix do phx.digest, gettext.extract
 | 
					  - mix do phx.digest, gettext.extract
 | 
				
			||||||
  - mix test.all
 | 
					  - mix test.all
 | 
				
			||||||
@@ -76,7 +80,8 @@ steps:
 | 
				
			|||||||
    mount:
 | 
					    mount:
 | 
				
			||||||
      - _build
 | 
					      - _build
 | 
				
			||||||
      - deps
 | 
					      - deps
 | 
				
			||||||
      - assets/node_modules/
 | 
					      - .npm
 | 
				
			||||||
 | 
					      - .mix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
services:
 | 
					services:
 | 
				
			||||||
- name: database
 | 
					- name: database
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
[
 | 
					[
 | 
				
			||||||
  import_deps: [:ecto, :phoenix],
 | 
					  import_deps: [:ecto, :ecto_sql, :phoenix],
 | 
				
			||||||
  inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
 | 
					  inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"],
 | 
				
			||||||
  subdirectories: ["priv/*/migrations"],
 | 
					  subdirectories: ["priv/*/migrations"],
 | 
				
			||||||
  plugins: [Phoenix.LiveView.HTMLFormatter]
 | 
					  plugins: [Phoenix.LiveView.HTMLFormatter]
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,3 @@
 | 
				
			|||||||
elixir 1.14.1-otp-25
 | 
					elixir 1.14.4-otp-25
 | 
				
			||||||
erlang 25.1.2
 | 
					erlang 25.3
 | 
				
			||||||
nodejs 16.18.0
 | 
					nodejs 18.15.0
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
FROM elixir:1.14.1-alpine AS build
 | 
					FROM elixir:1.14.4-alpine AS build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# install build dependencies
 | 
					# install build dependencies
 | 
				
			||||||
RUN apk add --no-cache build-base npm git python3
 | 
					RUN apk add --no-cache build-base npm git python3
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,17 +25,18 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
 | 
				
			|||||||
  100% { scale: 1.0; opacity: 1; }
 | 
					  100% { scale: 1.0; opacity: 1; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.phx-connected > #disconnect, #loading {
 | 
					// disconnect toast
 | 
				
			||||||
 | 
					.phx-connected > #disconnect {
 | 
				
			||||||
  opacity: 0 !important;
 | 
					  opacity: 0 !important;
 | 
				
			||||||
  pointer-events: none;
 | 
					  pointer-events: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.phx-loading:not(.phx-error) > #loading, .phx-error > #disconnect {
 | 
					.phx-error > #disconnect {
 | 
				
			||||||
  opacity: 0.95 !important;
 | 
					  opacity: 0.95 !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.invalid-feedback {
 | 
					.invalid-feedback {
 | 
				
			||||||
  color: #a94442;
 | 
					  color: #f36c69;
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  margin: -1rem 0 2rem;
 | 
					  margin: -1rem 0 2rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .checkbox {
 | 
					  .checkbox {
 | 
				
			||||||
 | 
					    @apply bg-primary-900;
 | 
				
			||||||
    -ms-transform: scale(1.5);
 | 
					    -ms-transform: scale(1.5);
 | 
				
			||||||
    -moz-transform: scale(1.5);
 | 
					    -moz-transform: scale(1.5);
 | 
				
			||||||
    -webkit-transform: scale(1.5);
 | 
					    -webkit-transform: scale(1.5);
 | 
				
			||||||
@@ -26,25 +27,31 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  .btn {
 | 
					  .btn {
 | 
				
			||||||
    @apply focus:outline-none px-4 py-2 rounded-lg;
 | 
					    @apply focus:outline-none px-4 py-2 rounded-lg;
 | 
				
			||||||
    @apply shadow-sm focus:shadow-lg;
 | 
					    @apply shadow-sm active:shadow-lg;
 | 
				
			||||||
    @apply border;
 | 
					    @apply border;
 | 
				
			||||||
    @apply transition-all duration-300 ease-in-out;
 | 
					    @apply transition-all duration-300 ease-in-out;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .btn-primary {
 | 
					  .btn-primary {
 | 
				
			||||||
    @apply bg-primary-900 focus:bg-primary-900 active:bg-primary-800;
 | 
					    @apply bg-primary-900 active:bg-primary-800;
 | 
				
			||||||
    @apply border-primary-900 hover:border-primary-800 active:border-primary-700;
 | 
					    @apply border-primary-900 hover:border-primary-800 active:border-primary-700;
 | 
				
			||||||
    @apply text-primary-400;
 | 
					    @apply text-primary-400;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn-secondary {
 | 
				
			||||||
 | 
					    @apply bg-primary-800 active:bg-primary-700;
 | 
				
			||||||
 | 
					    @apply border-primary-800 hover:border-primary-700 active:border-primary-600;
 | 
				
			||||||
 | 
					    @apply text-primary-400;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .btn-alert {
 | 
					  .btn-alert {
 | 
				
			||||||
    @apply bg-red-700 focus:bg-red-800 active:bg-red-900;
 | 
					    @apply bg-red-800 active:bg-red-900;
 | 
				
			||||||
    @apply border-red-700 focus:border-red-800 active:border-red-900;
 | 
					    @apply border-red-800 active:border-red-900;
 | 
				
			||||||
    @apply text-primary-300;
 | 
					    @apply text-primary-300;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .hr {
 | 
					  .hr {
 | 
				
			||||||
    @apply mx-auto border border-primary-600 w-full max-w-2xl;
 | 
					    @apply mx-auto border border-primary-600 w-full max-w-3xl;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .link {
 | 
					  .link {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,31 +25,25 @@ import 'phoenix_html'
 | 
				
			|||||||
// Establish Phoenix Socket and LiveView configuration.
 | 
					// Establish Phoenix Socket and LiveView configuration.
 | 
				
			||||||
import { Socket } from 'phoenix'
 | 
					import { Socket } from 'phoenix'
 | 
				
			||||||
import { LiveSocket } from 'phoenix_live_view'
 | 
					import { LiveSocket } from 'phoenix_live_view'
 | 
				
			||||||
import topbar from '../vendor/topbar'
 | 
					import topbar from 'topbar'
 | 
				
			||||||
 | 
					import Date from './date'
 | 
				
			||||||
 | 
					import DateTime from './datetime'
 | 
				
			||||||
import MaintainAttrs from './maintain_attrs'
 | 
					import MaintainAttrs from './maintain_attrs'
 | 
				
			||||||
import Alpine from 'alpinejs'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const csrfTokenElement = document.querySelector("meta[name='csrf-token']")
 | 
					const csrfTokenElement = document.querySelector("meta[name='csrf-token']")
 | 
				
			||||||
let csrfToken
 | 
					let csrfToken
 | 
				
			||||||
if (csrfTokenElement) { csrfToken = csrfTokenElement.getAttribute('content') }
 | 
					if (csrfTokenElement) { csrfToken = csrfTokenElement.getAttribute('content') }
 | 
				
			||||||
const liveSocket = new LiveSocket('/live', Socket, {
 | 
					const liveSocket = new LiveSocket('/live', Socket, {
 | 
				
			||||||
  dom: {
 | 
					 | 
				
			||||||
    onBeforeElUpdated (from, to) {
 | 
					 | 
				
			||||||
      if (from._x_dataStack) { window.Alpine.clone(from, to) }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  params: { _csrf_token: csrfToken },
 | 
					  params: { _csrf_token: csrfToken },
 | 
				
			||||||
  hooks: { MaintainAttrs }
 | 
					  hooks: { Date, DateTime, MaintainAttrs }
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// alpine.js
 | 
					 | 
				
			||||||
window.Alpine = Alpine
 | 
					 | 
				
			||||||
Alpine.start()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Show progress bar on live navigation and form submits
 | 
					// Show progress bar on live navigation and form submits
 | 
				
			||||||
topbar.config({ barThickness: 1, barColors: { 0: '#fff' }, shadowColor: 'rgba(0, 0, 0, .3)' })
 | 
					topbar.config({ barThickness: 1, barColors: { 0: '#fff' }, shadowColor: 'rgba(0, 0, 0, .3)' })
 | 
				
			||||||
window.addEventListener('phx:page-loading-start', info => topbar.show())
 | 
					window.addEventListener('phx:page-loading-start', info => topbar.show())
 | 
				
			||||||
window.addEventListener('phx:page-loading-stop', info => topbar.hide())
 | 
					window.addEventListener('phx:page-loading-stop', info => topbar.hide())
 | 
				
			||||||
 | 
					window.addEventListener('submit', info => topbar.show())
 | 
				
			||||||
 | 
					window.addEventListener('beforeunload', info => topbar.show())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// connect if there are any LiveViews on the page
 | 
					// connect if there are any LiveViews on the page
 | 
				
			||||||
liveSocket.connect()
 | 
					liveSocket.connect()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								assets/js/date.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								assets/js/date.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  displayDate (el) {
 | 
				
			||||||
 | 
					    const date =
 | 
				
			||||||
 | 
					      Intl.DateTimeFormat([], { timeZone: 'Etc/UTC', dateStyle: 'short' })
 | 
				
			||||||
 | 
					        .format(new Date(el.dateTime))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    el.innerText = date
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted () { this.displayDate(this.el) },
 | 
				
			||||||
 | 
					  updated () { this.displayDate(this.el) }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								assets/js/datetime.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								assets/js/datetime.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  displayDateTime (el) {
 | 
				
			||||||
 | 
					    const date =
 | 
				
			||||||
 | 
					      Intl.DateTimeFormat([], { dateStyle: 'short', timeStyle: 'long' })
 | 
				
			||||||
 | 
					        .format(new Date(el.dateTime))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    el.innerText = date
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted () { this.displayDateTime(this.el) },
 | 
				
			||||||
 | 
					  updated () { this.displayDateTime(this.el) }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15832
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15832
									
								
								assets/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -2,6 +2,10 @@
 | 
				
			|||||||
  "repository": {},
 | 
					  "repository": {},
 | 
				
			||||||
  "description": " ",
 | 
					  "description": " ",
 | 
				
			||||||
  "license": "MIT",
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "engines": {
 | 
				
			||||||
 | 
					    "node": "v18.15.0",
 | 
				
			||||||
 | 
					    "npm": "9.5.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "deploy": "NODE_ENV=production webpack --mode production",
 | 
					    "deploy": "NODE_ENV=production webpack --mode production",
 | 
				
			||||||
    "watch": "webpack --mode development --watch --watch-options-stdin",
 | 
					    "watch": "webpack --mode development --watch --watch-options-stdin",
 | 
				
			||||||
@@ -9,37 +13,35 @@
 | 
				
			|||||||
    "test": "standard"
 | 
					    "test": "standard"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@fontsource/nunito-sans": "^4.5.8",
 | 
					    "@fontsource/nunito-sans": "^4.5.10",
 | 
				
			||||||
    "@fortawesome/fontawesome-free": "^6.1.1",
 | 
					    "@fortawesome/fontawesome-free": "^6.4.0",
 | 
				
			||||||
    "alpinejs": "^3.10.2",
 | 
					 | 
				
			||||||
    "phoenix": "file:../deps/phoenix",
 | 
					    "phoenix": "file:../deps/phoenix",
 | 
				
			||||||
    "phoenix_html": "file:../deps/phoenix_html",
 | 
					    "phoenix_html": "file:../deps/phoenix_html",
 | 
				
			||||||
    "phoenix_live_view": "file:../deps/phoenix_live_view",
 | 
					    "phoenix_live_view": "file:../deps/phoenix_live_view",
 | 
				
			||||||
    "topbar": "^1.0.1"
 | 
					    "topbar": "^2.0.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@babel/core": "^7.17.10",
 | 
					    "@babel/core": "^7.21.4",
 | 
				
			||||||
    "@babel/preset-env": "^7.17.10",
 | 
					    "@babel/preset-env": "^7.21.4",
 | 
				
			||||||
    "autoprefixer": "^10.4.7",
 | 
					    "autoprefixer": "^10.4.14",
 | 
				
			||||||
    "babel-loader": "^8.2.5",
 | 
					    "babel-loader": "^9.1.2",
 | 
				
			||||||
    "copy-webpack-plugin": "^10.2.4",
 | 
					    "copy-webpack-plugin": "^11.0.0",
 | 
				
			||||||
    "css-loader": "^6.7.1",
 | 
					    "css-loader": "^6.7.3",
 | 
				
			||||||
    "css-minimizer-webpack-plugin": "^3.4.1",
 | 
					    "css-minimizer-webpack-plugin": "^5.0.0",
 | 
				
			||||||
    "file-loader": "^6.2.0",
 | 
					    "file-loader": "^6.2.0",
 | 
				
			||||||
    "hard-source-webpack-plugin": "^0.13.1",
 | 
					    "mini-css-extract-plugin": "^2.7.5",
 | 
				
			||||||
    "mini-css-extract-plugin": "^2.6.0",
 | 
					    "npm-check-updates": "^16.10.8",
 | 
				
			||||||
    "node-sass": "^7.0.1",
 | 
					    "postcss": "^8.4.21",
 | 
				
			||||||
    "postcss": "^8.4.13",
 | 
					    "postcss-import": "^15.1.0",
 | 
				
			||||||
    "postcss-import": "^14.1.0",
 | 
					    "postcss-loader": "^7.2.4",
 | 
				
			||||||
    "postcss-loader": "^6.2.1",
 | 
					    "postcss-preset-env": "^8.3.1",
 | 
				
			||||||
    "postcss-preset-env": "^7.5.0",
 | 
					    "sass": "^1.62.0",
 | 
				
			||||||
    "sass-loader": "^12.6.0",
 | 
					    "sass-loader": "^13.2.2",
 | 
				
			||||||
    "standard": "^17.0.0",
 | 
					    "standard": "^17.0.0",
 | 
				
			||||||
    "style-loader": "^3.3.1",
 | 
					    "tailwindcss": "^3.3.1",
 | 
				
			||||||
    "tailwindcss": "^3.0.24",
 | 
					    "terser-webpack-plugin": "^5.3.7",
 | 
				
			||||||
    "terser-webpack-plugin": "^5.3.1",
 | 
					    "webpack": "^5.79.0",
 | 
				
			||||||
    "webpack": "^5.72.0",
 | 
					    "webpack-cli": "^5.0.1",
 | 
				
			||||||
    "webpack-cli": "^4.9.2",
 | 
					    "webpack-dev-server": "^4.13.2"
 | 
				
			||||||
    "webpack-dev-server": "^4.9.0"
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										157
									
								
								assets/vendor/topbar.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										157
									
								
								assets/vendor/topbar.js
									
									
									
									
										vendored
									
									
								
							@@ -1,157 +0,0 @@
 | 
				
			|||||||
/**
 | 
					 | 
				
			||||||
 * @license MIT
 | 
					 | 
				
			||||||
 * topbar 1.0.0, 2021-01-06
 | 
					 | 
				
			||||||
 * https://buunguyen.github.io/topbar
 | 
					 | 
				
			||||||
 * Copyright (c) 2021 Buu Nguyen
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
(function (window, document) {
 | 
					 | 
				
			||||||
  "use strict";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // https://gist.github.com/paulirish/1579671
 | 
					 | 
				
			||||||
  (function () {
 | 
					 | 
				
			||||||
    var lastTime = 0;
 | 
					 | 
				
			||||||
    var vendors = ["ms", "moz", "webkit", "o"];
 | 
					 | 
				
			||||||
    for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
 | 
					 | 
				
			||||||
      window.requestAnimationFrame =
 | 
					 | 
				
			||||||
        window[vendors[x] + "RequestAnimationFrame"];
 | 
					 | 
				
			||||||
      window.cancelAnimationFrame =
 | 
					 | 
				
			||||||
        window[vendors[x] + "CancelAnimationFrame"] ||
 | 
					 | 
				
			||||||
        window[vendors[x] + "CancelRequestAnimationFrame"];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!window.requestAnimationFrame)
 | 
					 | 
				
			||||||
      window.requestAnimationFrame = function (callback, element) {
 | 
					 | 
				
			||||||
        var currTime = new Date().getTime();
 | 
					 | 
				
			||||||
        var timeToCall = Math.max(0, 16 - (currTime - lastTime));
 | 
					 | 
				
			||||||
        var id = window.setTimeout(function () {
 | 
					 | 
				
			||||||
          callback(currTime + timeToCall);
 | 
					 | 
				
			||||||
        }, timeToCall);
 | 
					 | 
				
			||||||
        lastTime = currTime + timeToCall;
 | 
					 | 
				
			||||||
        return id;
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    if (!window.cancelAnimationFrame)
 | 
					 | 
				
			||||||
      window.cancelAnimationFrame = function (id) {
 | 
					 | 
				
			||||||
        clearTimeout(id);
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
  })();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  var canvas,
 | 
					 | 
				
			||||||
    progressTimerId,
 | 
					 | 
				
			||||||
    fadeTimerId,
 | 
					 | 
				
			||||||
    currentProgress,
 | 
					 | 
				
			||||||
    showing,
 | 
					 | 
				
			||||||
    addEvent = function (elem, type, handler) {
 | 
					 | 
				
			||||||
      if (elem.addEventListener) elem.addEventListener(type, handler, false);
 | 
					 | 
				
			||||||
      else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
 | 
					 | 
				
			||||||
      else elem["on" + type] = handler;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    options = {
 | 
					 | 
				
			||||||
      autoRun: true,
 | 
					 | 
				
			||||||
      barThickness: 3,
 | 
					 | 
				
			||||||
      barColors: {
 | 
					 | 
				
			||||||
        0: "rgba(26,  188, 156, .9)",
 | 
					 | 
				
			||||||
        ".25": "rgba(52,  152, 219, .9)",
 | 
					 | 
				
			||||||
        ".50": "rgba(241, 196, 15,  .9)",
 | 
					 | 
				
			||||||
        ".75": "rgba(230, 126, 34,  .9)",
 | 
					 | 
				
			||||||
        "1.0": "rgba(211, 84,  0,   .9)",
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      shadowBlur: 10,
 | 
					 | 
				
			||||||
      shadowColor: "rgba(0,   0,   0,   .6)",
 | 
					 | 
				
			||||||
      className: null,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    repaint = function () {
 | 
					 | 
				
			||||||
      canvas.width = window.innerWidth;
 | 
					 | 
				
			||||||
      canvas.height = options.barThickness * 5; // need space for shadow
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      var ctx = canvas.getContext("2d");
 | 
					 | 
				
			||||||
      ctx.shadowBlur = options.shadowBlur;
 | 
					 | 
				
			||||||
      ctx.shadowColor = options.shadowColor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
 | 
					 | 
				
			||||||
      for (var stop in options.barColors)
 | 
					 | 
				
			||||||
        lineGradient.addColorStop(stop, options.barColors[stop]);
 | 
					 | 
				
			||||||
      ctx.lineWidth = options.barThickness;
 | 
					 | 
				
			||||||
      ctx.beginPath();
 | 
					 | 
				
			||||||
      ctx.moveTo(0, options.barThickness / 2);
 | 
					 | 
				
			||||||
      ctx.lineTo(
 | 
					 | 
				
			||||||
        Math.ceil(currentProgress * canvas.width),
 | 
					 | 
				
			||||||
        options.barThickness / 2
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      ctx.strokeStyle = lineGradient;
 | 
					 | 
				
			||||||
      ctx.stroke();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    createCanvas = function () {
 | 
					 | 
				
			||||||
      canvas = document.createElement("canvas");
 | 
					 | 
				
			||||||
      var style = canvas.style;
 | 
					 | 
				
			||||||
      style.position = "fixed";
 | 
					 | 
				
			||||||
      style.top = style.left = style.right = style.margin = style.padding = 0;
 | 
					 | 
				
			||||||
      style.zIndex = 100001;
 | 
					 | 
				
			||||||
      style.display = "none";
 | 
					 | 
				
			||||||
      if (options.className) canvas.classList.add(options.className);
 | 
					 | 
				
			||||||
      document.body.appendChild(canvas);
 | 
					 | 
				
			||||||
      addEvent(window, "resize", repaint);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    topbar = {
 | 
					 | 
				
			||||||
      config: function (opts) {
 | 
					 | 
				
			||||||
        for (var key in opts)
 | 
					 | 
				
			||||||
          if (options.hasOwnProperty(key)) options[key] = opts[key];
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      show: function () {
 | 
					 | 
				
			||||||
        if (showing) return;
 | 
					 | 
				
			||||||
        showing = true;
 | 
					 | 
				
			||||||
        if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
 | 
					 | 
				
			||||||
        if (!canvas) createCanvas();
 | 
					 | 
				
			||||||
        canvas.style.opacity = 1;
 | 
					 | 
				
			||||||
        canvas.style.display = "block";
 | 
					 | 
				
			||||||
        topbar.progress(0);
 | 
					 | 
				
			||||||
        if (options.autoRun) {
 | 
					 | 
				
			||||||
          (function loop() {
 | 
					 | 
				
			||||||
            progressTimerId = window.requestAnimationFrame(loop);
 | 
					 | 
				
			||||||
            topbar.progress(
 | 
					 | 
				
			||||||
              "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          })();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      progress: function (to) {
 | 
					 | 
				
			||||||
        if (typeof to === "undefined") return currentProgress;
 | 
					 | 
				
			||||||
        if (typeof to === "string") {
 | 
					 | 
				
			||||||
          to =
 | 
					 | 
				
			||||||
            (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
 | 
					 | 
				
			||||||
              ? currentProgress
 | 
					 | 
				
			||||||
              : 0) + parseFloat(to);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        currentProgress = to > 1 ? 1 : to;
 | 
					 | 
				
			||||||
        repaint();
 | 
					 | 
				
			||||||
        return currentProgress;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      hide: function () {
 | 
					 | 
				
			||||||
        if (!showing) return;
 | 
					 | 
				
			||||||
        showing = false;
 | 
					 | 
				
			||||||
        if (progressTimerId != null) {
 | 
					 | 
				
			||||||
          window.cancelAnimationFrame(progressTimerId);
 | 
					 | 
				
			||||||
          progressTimerId = null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        (function loop() {
 | 
					 | 
				
			||||||
          if (topbar.progress("+.1") >= 1) {
 | 
					 | 
				
			||||||
            canvas.style.opacity -= 0.05;
 | 
					 | 
				
			||||||
            if (canvas.style.opacity <= 0.05) {
 | 
					 | 
				
			||||||
              canvas.style.display = "none";
 | 
					 | 
				
			||||||
              fadeTimerId = null;
 | 
					 | 
				
			||||||
              return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          fadeTimerId = window.requestAnimationFrame(loop);
 | 
					 | 
				
			||||||
        })();
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (typeof module === "object" && typeof module.exports === "object") {
 | 
					 | 
				
			||||||
    module.exports = topbar;
 | 
					 | 
				
			||||||
  } else if (typeof define === "function" && define.amd) {
 | 
					 | 
				
			||||||
    define(function () {
 | 
					 | 
				
			||||||
      return topbar;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    this.topbar = topbar;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}.call(this, window, document));
 | 
					 | 
				
			||||||
@@ -45,7 +45,7 @@ module.exports = (env, options) => {
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
          test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
 | 
					          test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
 | 
				
			||||||
          type: 'asset/resource',
 | 
					          type: 'asset/resource',
 | 
				
			||||||
          generator: { filename: 'fonts/[name][ext]' }
 | 
					          generator: { filename: 'fonts/[name].[ext]' }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										33
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,3 +1,36 @@
 | 
				
			|||||||
 | 
					# v0.1.11
 | 
				
			||||||
 | 
					- Update dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.1.10
 | 
				
			||||||
 | 
					- Improve accessibility
 | 
				
			||||||
 | 
					- Code quality improvements
 | 
				
			||||||
 | 
					- Fix dates displaying incorrectly
 | 
				
			||||||
 | 
					- Add links to readme for github mirror
 | 
				
			||||||
 | 
					- Add license (whoops)
 | 
				
			||||||
 | 
					- Display links in note/context/step contents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.1.9
 | 
				
			||||||
 | 
					- Improve server log
 | 
				
			||||||
 | 
					- Style 大一點
 | 
				
			||||||
 | 
					- Various minor improvements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.1.8
 | 
				
			||||||
 | 
					- Fix bug with public registration
 | 
				
			||||||
 | 
					- Improve templates
 | 
				
			||||||
 | 
					- Improve invites, record usage
 | 
				
			||||||
 | 
					- Fix padding on more pages when using chrome
 | 
				
			||||||
 | 
					- Add oban metrics to server log and live dashboard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.1.7
 | 
				
			||||||
 | 
					- Update dependencies
 | 
				
			||||||
 | 
					- Show topbar on form submit/page refresh
 | 
				
			||||||
 | 
					- Make loading/reconnection less intrusive
 | 
				
			||||||
 | 
					- Add QR code for invite link
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# v0.1.6
 | 
				
			||||||
 | 
					- fix formatting in note/context/step contents
 | 
				
			||||||
 | 
					- add json export for data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# v0.1.5
 | 
					# v0.1.5
 | 
				
			||||||
- fix overflow on note/contexts/step contents
 | 
					- fix overflow on note/contexts/step contents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,8 @@ config :memex,
 | 
				
			|||||||
  ecto_repos: [Memex.Repo],
 | 
					  ecto_repos: [Memex.Repo],
 | 
				
			||||||
  generators: [binary_id: true]
 | 
					  generators: [binary_id: true]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					config :memex, Memex.Accounts, registration: System.get_env("REGISTRATION", "invite")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configures the endpoint
 | 
					# Configures the endpoint
 | 
				
			||||||
config :memex, MemexWeb.Endpoint,
 | 
					config :memex, MemexWeb.Endpoint,
 | 
				
			||||||
  url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"],
 | 
					  url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"],
 | 
				
			||||||
@@ -18,8 +20,7 @@ config :memex, MemexWeb.Endpoint,
 | 
				
			|||||||
  secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
 | 
					  secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
 | 
				
			||||||
  render_errors: [view: MemexWeb.ErrorView, accepts: ~w(html json), layout: false],
 | 
					  render_errors: [view: MemexWeb.ErrorView, accepts: ~w(html json), layout: false],
 | 
				
			||||||
  pubsub_server: Memex.PubSub,
 | 
					  pubsub_server: Memex.PubSub,
 | 
				
			||||||
  live_view: [signing_salt: "zOLgd3lr"],
 | 
					  live_view: [signing_salt: "zOLgd3lr"]
 | 
				
			||||||
  registration: System.get_env("REGISTRATION") || "invite"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
config :memex, Memex.Application, automigrate: false
 | 
					config :memex, Memex.Application, automigrate: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,13 +59,13 @@ config :memex, MemexWeb.Endpoint,
 | 
				
			|||||||
    patterns: [
 | 
					    patterns: [
 | 
				
			||||||
      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
 | 
					      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
 | 
				
			||||||
      ~r"priv/gettext/.*(po)$",
 | 
					      ~r"priv/gettext/.*(po)$",
 | 
				
			||||||
      ~r"lib/memex_web/(live|views)/.*(ex)$",
 | 
					      ~r"lib/memex_web/*/.*(ex)$"
 | 
				
			||||||
      ~r"lib/memex_web/templates/.*(eex)$"
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Do not include metadata nor timestamps in development logs
 | 
					config :logger, :console,
 | 
				
			||||||
config :logger, :console, format: "[$level] $message\n"
 | 
					  format: "[$level] $message $metadata\n\n",
 | 
				
			||||||
 | 
					  metadata: [:data]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Set a higher stacktrace during development. Avoid configuring such
 | 
					# Set a higher stacktrace during development. Avoid configuring such
 | 
				
			||||||
# in production as building large stacktraces may be expensive.
 | 
					# in production as building large stacktraces may be expensive.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,17 +13,18 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
 | 
				
			|||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Set default locale
 | 
					# Set default locale
 | 
				
			||||||
config :gettext, :default_locale, System.get_env("LOCALE") || "en_US"
 | 
					config :gettext, :default_locale, System.get_env("LOCALE", "en_US")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
maybe_ipv6 = if System.get_env("ECTO_IPV6") == "true", do: [:inet6], else: []
 | 
					maybe_ipv6 = if System.get_env("ECTO_IPV6") == "true", do: [:inet6], else: []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
database_url =
 | 
					database_url =
 | 
				
			||||||
  if config_env() == :test do
 | 
					  if config_env() == :test do
 | 
				
			||||||
    System.get_env("TEST_DATABASE_URL") ||
 | 
					    System.get_env(
 | 
				
			||||||
 | 
					      "TEST_DATABASE_URL",
 | 
				
			||||||
      "ecto://postgres:postgres@localhost/memex_test#{System.get_env("MIX_TEST_PARTITION")}"
 | 
					      "ecto://postgres:postgres@localhost/memex_test#{System.get_env("MIX_TEST_PARTITION")}"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
  else
 | 
					  else
 | 
				
			||||||
    System.get_env("DATABASE_URL") ||
 | 
					    System.get_env("DATABASE_URL", "ecto://postgres:postgres@memex-db/memex")
 | 
				
			||||||
      "ecto://postgres:postgres@memex-db/memex"
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
host =
 | 
					host =
 | 
				
			||||||
@@ -38,7 +39,7 @@ interface =
 | 
				
			|||||||
config :memex, Memex.Repo,
 | 
					config :memex, Memex.Repo,
 | 
				
			||||||
  # ssl: true,
 | 
					  # ssl: true,
 | 
				
			||||||
  url: database_url,
 | 
					  url: database_url,
 | 
				
			||||||
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
 | 
					  pool_size: String.to_integer(System.get_env("POOL_SIZE", "10")),
 | 
				
			||||||
  socket_options: maybe_ipv6
 | 
					  socket_options: maybe_ipv6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
config :memex, MemexWeb.Endpoint,
 | 
					config :memex, MemexWeb.Endpoint,
 | 
				
			||||||
@@ -47,10 +48,13 @@ config :memex, MemexWeb.Endpoint,
 | 
				
			|||||||
    # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
 | 
					    # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
 | 
				
			||||||
    # for details about using IPv6 vs IPv4 and loopback vs public addresses.
 | 
					    # for details about using IPv6 vs IPv4 and loopback vs public addresses.
 | 
				
			||||||
    ip: interface,
 | 
					    ip: interface,
 | 
				
			||||||
    port: String.to_integer(System.get_env("PORT") || "4000")
 | 
					    port: String.to_integer(System.get_env("PORT", "4000"))
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  server: true,
 | 
					  server: true
 | 
				
			||||||
  registration: System.get_env("REGISTRATION") || "invite"
 | 
					
 | 
				
			||||||
 | 
					if config_env() in [:dev, :prod] do
 | 
				
			||||||
 | 
					  config :memex, Memex.Accounts, registration: System.get_env("REGISTRATION", "invite")
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if config_env() == :prod do
 | 
					if config_env() == :prod do
 | 
				
			||||||
  # The secret key base is used to sign/encrypt cookies and other secrets.
 | 
					  # The secret key base is used to sign/encrypt cookies and other secrets.
 | 
				
			||||||
@@ -74,12 +78,12 @@ if config_env() == :prod do
 | 
				
			|||||||
  config :memex, Memex.Mailer,
 | 
					  config :memex, Memex.Mailer,
 | 
				
			||||||
    adapter: Swoosh.Adapters.SMTP,
 | 
					    adapter: Swoosh.Adapters.SMTP,
 | 
				
			||||||
    relay: System.get_env("SMTP_HOST") || raise("No SMTP_HOST set!"),
 | 
					    relay: System.get_env("SMTP_HOST") || raise("No SMTP_HOST set!"),
 | 
				
			||||||
    port: System.get_env("SMTP_PORT") || 587,
 | 
					    port: System.get_env("SMTP_PORT", "587"),
 | 
				
			||||||
    username: System.get_env("SMTP_USERNAME") || raise("No SMTP_USERNAME set!"),
 | 
					    username: System.get_env("SMTP_USERNAME") || raise("No SMTP_USERNAME set!"),
 | 
				
			||||||
    password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"),
 | 
					    password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"),
 | 
				
			||||||
    ssl: System.get_env("SMTP_SSL") == "true",
 | 
					    ssl: System.get_env("SMTP_SSL") == "true",
 | 
				
			||||||
    email_from: System.get_env("EMAIL_FROM") || "no-reply@#{System.get_env("HOST")}",
 | 
					    email_from: System.get_env("EMAIL_FROM", "no-reply@#{System.get_env("HOST")}"),
 | 
				
			||||||
    email_name: System.get_env("EMAIL_NAME") || "memEx"
 | 
					    email_name: System.get_env("EMAIL_NAME", "memEx")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # ## Using releases
 | 
					  # ## Using releases
 | 
				
			||||||
  #
 | 
					  #
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,9 @@ config :memex, MemexWeb.Endpoint,
 | 
				
			|||||||
# In test we don't send emails.
 | 
					# In test we don't send emails.
 | 
				
			||||||
config :memex, Memex.Mailer, adapter: Swoosh.Adapters.Test
 | 
					config :memex, Memex.Mailer, adapter: Swoosh.Adapters.Test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Don't require invites for signups
 | 
				
			||||||
 | 
					config :memex, Memex.Accounts, registration: "public"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Print only warnings and errors during test
 | 
					# Print only warnings and errors during test
 | 
				
			||||||
config :logger, level: :warn
 | 
					config :logger, level: :warn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										660
									
								
								lib/license.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										660
									
								
								lib/license.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,660 @@
 | 
				
			|||||||
 | 
					### GNU AFFERO GENERAL PUBLIC LICENSE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Version 3, 19 November 2007
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (C) 2007 Free Software Foundation, Inc.
 | 
				
			||||||
 | 
					<https://fsf.org/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Everyone is permitted to copy and distribute verbatim copies of this
 | 
				
			||||||
 | 
					license document, but changing it is not allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Preamble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The GNU Affero General Public License is a free, copyleft license for
 | 
				
			||||||
 | 
					software and other kinds of works, specifically designed to ensure
 | 
				
			||||||
 | 
					cooperation with the community in the case of network server software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The licenses for most software and other practical works are designed
 | 
				
			||||||
 | 
					to take away your freedom to share and change the works. By contrast,
 | 
				
			||||||
 | 
					our General Public Licenses are intended to guarantee your freedom to
 | 
				
			||||||
 | 
					share and change all versions of a program--to make sure it remains
 | 
				
			||||||
 | 
					free software for all its users.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When we speak of free software, we are referring to freedom, not
 | 
				
			||||||
 | 
					price. Our General Public Licenses are designed to make sure that you
 | 
				
			||||||
 | 
					have the freedom to distribute copies of free software (and charge for
 | 
				
			||||||
 | 
					them if you wish), that you receive source code or can get it if you
 | 
				
			||||||
 | 
					want it, that you can change the software or use pieces of it in new
 | 
				
			||||||
 | 
					free programs, and that you know you can do these things.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Developers that use our General Public Licenses protect your rights
 | 
				
			||||||
 | 
					with two steps: (1) assert copyright on the software, and (2) offer
 | 
				
			||||||
 | 
					you this License which gives you legal permission to copy, distribute
 | 
				
			||||||
 | 
					and/or modify the software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A secondary benefit of defending all users' freedom is that
 | 
				
			||||||
 | 
					improvements made in alternate versions of the program, if they
 | 
				
			||||||
 | 
					receive widespread use, become available for other developers to
 | 
				
			||||||
 | 
					incorporate. Many developers of free software are heartened and
 | 
				
			||||||
 | 
					encouraged by the resulting cooperation. However, in the case of
 | 
				
			||||||
 | 
					software used on network servers, this result may fail to come about.
 | 
				
			||||||
 | 
					The GNU General Public License permits making a modified version and
 | 
				
			||||||
 | 
					letting the public access it on a server without ever releasing its
 | 
				
			||||||
 | 
					source code to the public.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The GNU Affero General Public License is designed specifically to
 | 
				
			||||||
 | 
					ensure that, in such cases, the modified source code becomes available
 | 
				
			||||||
 | 
					to the community. It requires the operator of a network server to
 | 
				
			||||||
 | 
					provide the source code of the modified version running there to the
 | 
				
			||||||
 | 
					users of that server. Therefore, public use of a modified version, on
 | 
				
			||||||
 | 
					a publicly accessible server, gives the public access to the source
 | 
				
			||||||
 | 
					code of the modified version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An older license, called the Affero General Public License and
 | 
				
			||||||
 | 
					published by Affero, was designed to accomplish similar goals. This is
 | 
				
			||||||
 | 
					a different license, not a version of the Affero GPL, but Affero has
 | 
				
			||||||
 | 
					released a new version of the Affero GPL which permits relicensing
 | 
				
			||||||
 | 
					under this license.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The precise terms and conditions for copying, distribution and
 | 
				
			||||||
 | 
					modification follow.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### TERMS AND CONDITIONS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 0. Definitions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"This License" refers to version 3 of the GNU Affero General Public
 | 
				
			||||||
 | 
					License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"Copyright" also means copyright-like laws that apply to other kinds
 | 
				
			||||||
 | 
					of works, such as semiconductor masks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"The Program" refers to any copyrightable work licensed under this
 | 
				
			||||||
 | 
					License. Each licensee is addressed as "you". "Licensees" and
 | 
				
			||||||
 | 
					"recipients" may be individuals or organizations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To "modify" a work means to copy from or adapt all or part of the work
 | 
				
			||||||
 | 
					in a fashion requiring copyright permission, other than the making of
 | 
				
			||||||
 | 
					an exact copy. The resulting work is called a "modified version" of
 | 
				
			||||||
 | 
					the earlier work or a work "based on" the earlier work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A "covered work" means either the unmodified Program or a work based
 | 
				
			||||||
 | 
					on the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To "propagate" a work means to do anything with it that, without
 | 
				
			||||||
 | 
					permission, would make you directly or secondarily liable for
 | 
				
			||||||
 | 
					infringement under applicable copyright law, except executing it on a
 | 
				
			||||||
 | 
					computer or modifying a private copy. Propagation includes copying,
 | 
				
			||||||
 | 
					distribution (with or without modification), making available to the
 | 
				
			||||||
 | 
					public, and in some countries other activities as well.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To "convey" a work means any kind of propagation that enables other
 | 
				
			||||||
 | 
					parties to make or receive copies. Mere interaction with a user
 | 
				
			||||||
 | 
					through a computer network, with no transfer of a copy, is not
 | 
				
			||||||
 | 
					conveying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An interactive user interface displays "Appropriate Legal Notices" to
 | 
				
			||||||
 | 
					the extent that it includes a convenient and prominently visible
 | 
				
			||||||
 | 
					feature that (1) displays an appropriate copyright notice, and (2)
 | 
				
			||||||
 | 
					tells the user that there is no warranty for the work (except to the
 | 
				
			||||||
 | 
					extent that warranties are provided), that licensees may convey the
 | 
				
			||||||
 | 
					work under this License, and how to view a copy of this License. If
 | 
				
			||||||
 | 
					the interface presents a list of user commands or options, such as a
 | 
				
			||||||
 | 
					menu, a prominent item in the list meets this criterion.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 1. Source Code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The "source code" for a work means the preferred form of the work for
 | 
				
			||||||
 | 
					making modifications to it. "Object code" means any non-source form of
 | 
				
			||||||
 | 
					a work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A "Standard Interface" means an interface that either is an official
 | 
				
			||||||
 | 
					standard defined by a recognized standards body, or, in the case of
 | 
				
			||||||
 | 
					interfaces specified for a particular programming language, one that
 | 
				
			||||||
 | 
					is widely used among developers working in that language.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The "System Libraries" of an executable work include anything, other
 | 
				
			||||||
 | 
					than the work as a whole, that (a) is included in the normal form of
 | 
				
			||||||
 | 
					packaging a Major Component, but which is not part of that Major
 | 
				
			||||||
 | 
					Component, and (b) serves only to enable use of the work with that
 | 
				
			||||||
 | 
					Major Component, or to implement a Standard Interface for which an
 | 
				
			||||||
 | 
					implementation is available to the public in source code form. A
 | 
				
			||||||
 | 
					"Major Component", in this context, means a major essential component
 | 
				
			||||||
 | 
					(kernel, window system, and so on) of the specific operating system
 | 
				
			||||||
 | 
					(if any) on which the executable work runs, or a compiler used to
 | 
				
			||||||
 | 
					produce the work, or an object code interpreter used to run it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The "Corresponding Source" for a work in object code form means all
 | 
				
			||||||
 | 
					the source code needed to generate, install, and (for an executable
 | 
				
			||||||
 | 
					work) run the object code and to modify the work, including scripts to
 | 
				
			||||||
 | 
					control those activities. However, it does not include the work's
 | 
				
			||||||
 | 
					System Libraries, or general-purpose tools or generally available free
 | 
				
			||||||
 | 
					programs which are used unmodified in performing those activities but
 | 
				
			||||||
 | 
					which are not part of the work. For example, Corresponding Source
 | 
				
			||||||
 | 
					includes interface definition files associated with source files for
 | 
				
			||||||
 | 
					the work, and the source code for shared libraries and dynamically
 | 
				
			||||||
 | 
					linked subprograms that the work is specifically designed to require,
 | 
				
			||||||
 | 
					such as by intimate data communication or control flow between those
 | 
				
			||||||
 | 
					subprograms and other parts of the work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Corresponding Source need not include anything that users can
 | 
				
			||||||
 | 
					regenerate automatically from other parts of the Corresponding Source.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Corresponding Source for a work in source code form is that same
 | 
				
			||||||
 | 
					work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 2. Basic Permissions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All rights granted under this License are granted for the term of
 | 
				
			||||||
 | 
					copyright on the Program, and are irrevocable provided the stated
 | 
				
			||||||
 | 
					conditions are met. This License explicitly affirms your unlimited
 | 
				
			||||||
 | 
					permission to run the unmodified Program. The output from running a
 | 
				
			||||||
 | 
					covered work is covered by this License only if the output, given its
 | 
				
			||||||
 | 
					content, constitutes a covered work. This License acknowledges your
 | 
				
			||||||
 | 
					rights of fair use or other equivalent, as provided by copyright law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may make, run and propagate covered works that you do not convey,
 | 
				
			||||||
 | 
					without conditions so long as your license otherwise remains in force.
 | 
				
			||||||
 | 
					You may convey covered works to others for the sole purpose of having
 | 
				
			||||||
 | 
					them make modifications exclusively for you, or provide you with
 | 
				
			||||||
 | 
					facilities for running those works, provided that you comply with the
 | 
				
			||||||
 | 
					terms of this License in conveying all material for which you do not
 | 
				
			||||||
 | 
					control copyright. Those thus making or running the covered works for
 | 
				
			||||||
 | 
					you must do so exclusively on your behalf, under your direction and
 | 
				
			||||||
 | 
					control, on terms that prohibit them from making any copies of your
 | 
				
			||||||
 | 
					copyrighted material outside their relationship with you.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Conveying under any other circumstances is permitted solely under the
 | 
				
			||||||
 | 
					conditions stated below. Sublicensing is not allowed; section 10 makes
 | 
				
			||||||
 | 
					it unnecessary.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					No covered work shall be deemed part of an effective technological
 | 
				
			||||||
 | 
					measure under any applicable law fulfilling obligations under article
 | 
				
			||||||
 | 
					11 of the WIPO copyright treaty adopted on 20 December 1996, or
 | 
				
			||||||
 | 
					similar laws prohibiting or restricting circumvention of such
 | 
				
			||||||
 | 
					measures.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When you convey a covered work, you waive any legal power to forbid
 | 
				
			||||||
 | 
					circumvention of technological measures to the extent such
 | 
				
			||||||
 | 
					circumvention is effected by exercising rights under this License with
 | 
				
			||||||
 | 
					respect to the covered work, and you disclaim any intention to limit
 | 
				
			||||||
 | 
					operation or modification of the work as a means of enforcing, against
 | 
				
			||||||
 | 
					the work's users, your or third parties' legal rights to forbid
 | 
				
			||||||
 | 
					circumvention of technological measures.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 4. Conveying Verbatim Copies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may convey verbatim copies of the Program's source code as you
 | 
				
			||||||
 | 
					receive it, in any medium, provided that you conspicuously and
 | 
				
			||||||
 | 
					appropriately publish on each copy an appropriate copyright notice;
 | 
				
			||||||
 | 
					keep intact all notices stating that this License and any
 | 
				
			||||||
 | 
					non-permissive terms added in accord with section 7 apply to the code;
 | 
				
			||||||
 | 
					keep intact all notices of the absence of any warranty; and give all
 | 
				
			||||||
 | 
					recipients a copy of this License along with the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may charge any price or no price for each copy that you convey,
 | 
				
			||||||
 | 
					and you may offer support or warranty protection for a fee.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 5. Conveying Modified Source Versions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may convey a work based on the Program, or the modifications to
 | 
				
			||||||
 | 
					produce it from the Program, in the form of source code under the
 | 
				
			||||||
 | 
					terms of section 4, provided that you also meet all of these
 | 
				
			||||||
 | 
					conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   a) The work must carry prominent notices stating that you modified
 | 
				
			||||||
 | 
					    it, and giving a relevant date.
 | 
				
			||||||
 | 
					-   b) The work must carry prominent notices stating that it is
 | 
				
			||||||
 | 
					    released under this License and any conditions added under
 | 
				
			||||||
 | 
					    section 7. This requirement modifies the requirement in section 4
 | 
				
			||||||
 | 
					    to "keep intact all notices".
 | 
				
			||||||
 | 
					-   c) You must license the entire work, as a whole, under this
 | 
				
			||||||
 | 
					    License to anyone who comes into possession of a copy. This
 | 
				
			||||||
 | 
					    License will therefore apply, along with any applicable section 7
 | 
				
			||||||
 | 
					    additional terms, to the whole of the work, and all its parts,
 | 
				
			||||||
 | 
					    regardless of how they are packaged. This License gives no
 | 
				
			||||||
 | 
					    permission to license the work in any other way, but it does not
 | 
				
			||||||
 | 
					    invalidate such permission if you have separately received it.
 | 
				
			||||||
 | 
					-   d) If the work has interactive user interfaces, each must display
 | 
				
			||||||
 | 
					    Appropriate Legal Notices; however, if the Program has interactive
 | 
				
			||||||
 | 
					    interfaces that do not display Appropriate Legal Notices, your
 | 
				
			||||||
 | 
					    work need not make them do so.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A compilation of a covered work with other separate and independent
 | 
				
			||||||
 | 
					works, which are not by their nature extensions of the covered work,
 | 
				
			||||||
 | 
					and which are not combined with it such as to form a larger program,
 | 
				
			||||||
 | 
					in or on a volume of a storage or distribution medium, is called an
 | 
				
			||||||
 | 
					"aggregate" if the compilation and its resulting copyright are not
 | 
				
			||||||
 | 
					used to limit the access or legal rights of the compilation's users
 | 
				
			||||||
 | 
					beyond what the individual works permit. Inclusion of a covered work
 | 
				
			||||||
 | 
					in an aggregate does not cause this License to apply to the other
 | 
				
			||||||
 | 
					parts of the aggregate.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 6. Conveying Non-Source Forms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may convey a covered work in object code form under the terms of
 | 
				
			||||||
 | 
					sections 4 and 5, provided that you also convey the machine-readable
 | 
				
			||||||
 | 
					Corresponding Source under the terms of this License, in one of these
 | 
				
			||||||
 | 
					ways:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   a) Convey the object code in, or embodied in, a physical product
 | 
				
			||||||
 | 
					    (including a physical distribution medium), accompanied by the
 | 
				
			||||||
 | 
					    Corresponding Source fixed on a durable physical medium
 | 
				
			||||||
 | 
					    customarily used for software interchange.
 | 
				
			||||||
 | 
					-   b) Convey the object code in, or embodied in, a physical product
 | 
				
			||||||
 | 
					    (including a physical distribution medium), accompanied by a
 | 
				
			||||||
 | 
					    written offer, valid for at least three years and valid for as
 | 
				
			||||||
 | 
					    long as you offer spare parts or customer support for that product
 | 
				
			||||||
 | 
					    model, to give anyone who possesses the object code either (1) a
 | 
				
			||||||
 | 
					    copy of the Corresponding Source for all the software in the
 | 
				
			||||||
 | 
					    product that is covered by this License, on a durable physical
 | 
				
			||||||
 | 
					    medium customarily used for software interchange, for a price no
 | 
				
			||||||
 | 
					    more than your reasonable cost of physically performing this
 | 
				
			||||||
 | 
					    conveying of source, or (2) access to copy the Corresponding
 | 
				
			||||||
 | 
					    Source from a network server at no charge.
 | 
				
			||||||
 | 
					-   c) Convey individual copies of the object code with a copy of the
 | 
				
			||||||
 | 
					    written offer to provide the Corresponding Source. This
 | 
				
			||||||
 | 
					    alternative is allowed only occasionally and noncommercially, and
 | 
				
			||||||
 | 
					    only if you received the object code with such an offer, in accord
 | 
				
			||||||
 | 
					    with subsection 6b.
 | 
				
			||||||
 | 
					-   d) Convey the object code by offering access from a designated
 | 
				
			||||||
 | 
					    place (gratis or for a charge), and offer equivalent access to the
 | 
				
			||||||
 | 
					    Corresponding Source in the same way through the same place at no
 | 
				
			||||||
 | 
					    further charge. You need not require recipients to copy the
 | 
				
			||||||
 | 
					    Corresponding Source along with the object code. If the place to
 | 
				
			||||||
 | 
					    copy the object code is a network server, the Corresponding Source
 | 
				
			||||||
 | 
					    may be on a different server (operated by you or a third party)
 | 
				
			||||||
 | 
					    that supports equivalent copying facilities, provided you maintain
 | 
				
			||||||
 | 
					    clear directions next to the object code saying where to find the
 | 
				
			||||||
 | 
					    Corresponding Source. Regardless of what server hosts the
 | 
				
			||||||
 | 
					    Corresponding Source, you remain obligated to ensure that it is
 | 
				
			||||||
 | 
					    available for as long as needed to satisfy these requirements.
 | 
				
			||||||
 | 
					-   e) Convey the object code using peer-to-peer transmission,
 | 
				
			||||||
 | 
					    provided you inform other peers where the object code and
 | 
				
			||||||
 | 
					    Corresponding Source of the work are being offered to the general
 | 
				
			||||||
 | 
					    public at no charge under subsection 6d.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A separable portion of the object code, whose source code is excluded
 | 
				
			||||||
 | 
					from the Corresponding Source as a System Library, need not be
 | 
				
			||||||
 | 
					included in conveying the object code work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A "User Product" is either (1) a "consumer product", which means any
 | 
				
			||||||
 | 
					tangible personal property which is normally used for personal,
 | 
				
			||||||
 | 
					family, or household purposes, or (2) anything designed or sold for
 | 
				
			||||||
 | 
					incorporation into a dwelling. In determining whether a product is a
 | 
				
			||||||
 | 
					consumer product, doubtful cases shall be resolved in favor of
 | 
				
			||||||
 | 
					coverage. For a particular product received by a particular user,
 | 
				
			||||||
 | 
					"normally used" refers to a typical or common use of that class of
 | 
				
			||||||
 | 
					product, regardless of the status of the particular user or of the way
 | 
				
			||||||
 | 
					in which the particular user actually uses, or expects or is expected
 | 
				
			||||||
 | 
					to use, the product. A product is a consumer product regardless of
 | 
				
			||||||
 | 
					whether the product has substantial commercial, industrial or
 | 
				
			||||||
 | 
					non-consumer uses, unless such uses represent the only significant
 | 
				
			||||||
 | 
					mode of use of the product.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"Installation Information" for a User Product means any methods,
 | 
				
			||||||
 | 
					procedures, authorization keys, or other information required to
 | 
				
			||||||
 | 
					install and execute modified versions of a covered work in that User
 | 
				
			||||||
 | 
					Product from a modified version of its Corresponding Source. The
 | 
				
			||||||
 | 
					information must suffice to ensure that the continued functioning of
 | 
				
			||||||
 | 
					the modified object code is in no case prevented or interfered with
 | 
				
			||||||
 | 
					solely because modification has been made.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you convey an object code work under this section in, or with, or
 | 
				
			||||||
 | 
					specifically for use in, a User Product, and the conveying occurs as
 | 
				
			||||||
 | 
					part of a transaction in which the right of possession and use of the
 | 
				
			||||||
 | 
					User Product is transferred to the recipient in perpetuity or for a
 | 
				
			||||||
 | 
					fixed term (regardless of how the transaction is characterized), the
 | 
				
			||||||
 | 
					Corresponding Source conveyed under this section must be accompanied
 | 
				
			||||||
 | 
					by the Installation Information. But this requirement does not apply
 | 
				
			||||||
 | 
					if neither you nor any third party retains the ability to install
 | 
				
			||||||
 | 
					modified object code on the User Product (for example, the work has
 | 
				
			||||||
 | 
					been installed in ROM).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The requirement to provide Installation Information does not include a
 | 
				
			||||||
 | 
					requirement to continue to provide support service, warranty, or
 | 
				
			||||||
 | 
					updates for a work that has been modified or installed by the
 | 
				
			||||||
 | 
					recipient, or for the User Product in which it has been modified or
 | 
				
			||||||
 | 
					installed. Access to a network may be denied when the modification
 | 
				
			||||||
 | 
					itself materially and adversely affects the operation of the network
 | 
				
			||||||
 | 
					or violates the rules and protocols for communication across the
 | 
				
			||||||
 | 
					network.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Corresponding Source conveyed, and Installation Information provided,
 | 
				
			||||||
 | 
					in accord with this section must be in a format that is publicly
 | 
				
			||||||
 | 
					documented (and with an implementation available to the public in
 | 
				
			||||||
 | 
					source code form), and must require no special password or key for
 | 
				
			||||||
 | 
					unpacking, reading or copying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 7. Additional Terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"Additional permissions" are terms that supplement the terms of this
 | 
				
			||||||
 | 
					License by making exceptions from one or more of its conditions.
 | 
				
			||||||
 | 
					Additional permissions that are applicable to the entire Program shall
 | 
				
			||||||
 | 
					be treated as though they were included in this License, to the extent
 | 
				
			||||||
 | 
					that they are valid under applicable law. If additional permissions
 | 
				
			||||||
 | 
					apply only to part of the Program, that part may be used separately
 | 
				
			||||||
 | 
					under those permissions, but the entire Program remains governed by
 | 
				
			||||||
 | 
					this License without regard to the additional permissions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When you convey a copy of a covered work, you may at your option
 | 
				
			||||||
 | 
					remove any additional permissions from that copy, or from any part of
 | 
				
			||||||
 | 
					it. (Additional permissions may be written to require their own
 | 
				
			||||||
 | 
					removal in certain cases when you modify the work.) You may place
 | 
				
			||||||
 | 
					additional permissions on material, added by you to a covered work,
 | 
				
			||||||
 | 
					for which you have or can give appropriate copyright permission.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Notwithstanding any other provision of this License, for material you
 | 
				
			||||||
 | 
					add to a covered work, you may (if authorized by the copyright holders
 | 
				
			||||||
 | 
					of that material) supplement the terms of this License with terms:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-   a) Disclaiming warranty or limiting liability differently from the
 | 
				
			||||||
 | 
					    terms of sections 15 and 16 of this License; or
 | 
				
			||||||
 | 
					-   b) Requiring preservation of specified reasonable legal notices or
 | 
				
			||||||
 | 
					    author attributions in that material or in the Appropriate Legal
 | 
				
			||||||
 | 
					    Notices displayed by works containing it; or
 | 
				
			||||||
 | 
					-   c) Prohibiting misrepresentation of the origin of that material,
 | 
				
			||||||
 | 
					    or requiring that modified versions of such material be marked in
 | 
				
			||||||
 | 
					    reasonable ways as different from the original version; or
 | 
				
			||||||
 | 
					-   d) Limiting the use for publicity purposes of names of licensors
 | 
				
			||||||
 | 
					    or authors of the material; or
 | 
				
			||||||
 | 
					-   e) Declining to grant rights under trademark law for use of some
 | 
				
			||||||
 | 
					    trade names, trademarks, or service marks; or
 | 
				
			||||||
 | 
					-   f) Requiring indemnification of licensors and authors of that
 | 
				
			||||||
 | 
					    material by anyone who conveys the material (or modified versions
 | 
				
			||||||
 | 
					    of it) with contractual assumptions of liability to the recipient,
 | 
				
			||||||
 | 
					    for any liability that these contractual assumptions directly
 | 
				
			||||||
 | 
					    impose on those licensors and authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All other non-permissive additional terms are considered "further
 | 
				
			||||||
 | 
					restrictions" within the meaning of section 10. If the Program as you
 | 
				
			||||||
 | 
					received it, or any part of it, contains a notice stating that it is
 | 
				
			||||||
 | 
					governed by this License along with a term that is a further
 | 
				
			||||||
 | 
					restriction, you may remove that term. If a license document contains
 | 
				
			||||||
 | 
					a further restriction but permits relicensing or conveying under this
 | 
				
			||||||
 | 
					License, you may add to a covered work material governed by the terms
 | 
				
			||||||
 | 
					of that license document, provided that the further restriction does
 | 
				
			||||||
 | 
					not survive such relicensing or conveying.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you add terms to a covered work in accord with this section, you
 | 
				
			||||||
 | 
					must place, in the relevant source files, a statement of the
 | 
				
			||||||
 | 
					additional terms that apply to those files, or a notice indicating
 | 
				
			||||||
 | 
					where to find the applicable terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Additional terms, permissive or non-permissive, may be stated in the
 | 
				
			||||||
 | 
					form of a separately written license, or stated as exceptions; the
 | 
				
			||||||
 | 
					above requirements apply either way.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 8. Termination.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may not propagate or modify a covered work except as expressly
 | 
				
			||||||
 | 
					provided under this License. Any attempt otherwise to propagate or
 | 
				
			||||||
 | 
					modify it is void, and will automatically terminate your rights under
 | 
				
			||||||
 | 
					this License (including any patent licenses granted under the third
 | 
				
			||||||
 | 
					paragraph of section 11).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					However, if you cease all violation of this License, then your license
 | 
				
			||||||
 | 
					from a particular copyright holder is reinstated (a) provisionally,
 | 
				
			||||||
 | 
					unless and until the copyright holder explicitly and finally
 | 
				
			||||||
 | 
					terminates your license, and (b) permanently, if the copyright holder
 | 
				
			||||||
 | 
					fails to notify you of the violation by some reasonable means prior to
 | 
				
			||||||
 | 
					60 days after the cessation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Moreover, your license from a particular copyright holder is
 | 
				
			||||||
 | 
					reinstated permanently if the copyright holder notifies you of the
 | 
				
			||||||
 | 
					violation by some reasonable means, this is the first time you have
 | 
				
			||||||
 | 
					received notice of violation of this License (for any work) from that
 | 
				
			||||||
 | 
					copyright holder, and you cure the violation prior to 30 days after
 | 
				
			||||||
 | 
					your receipt of the notice.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Termination of your rights under this section does not terminate the
 | 
				
			||||||
 | 
					licenses of parties who have received copies or rights from you under
 | 
				
			||||||
 | 
					this License. If your rights have been terminated and not permanently
 | 
				
			||||||
 | 
					reinstated, you do not qualify to receive new licenses for the same
 | 
				
			||||||
 | 
					material under section 10.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 9. Acceptance Not Required for Having Copies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You are not required to accept this License in order to receive or run
 | 
				
			||||||
 | 
					a copy of the Program. Ancillary propagation of a covered work
 | 
				
			||||||
 | 
					occurring solely as a consequence of using peer-to-peer transmission
 | 
				
			||||||
 | 
					to receive a copy likewise does not require acceptance. However,
 | 
				
			||||||
 | 
					nothing other than this License grants you permission to propagate or
 | 
				
			||||||
 | 
					modify any covered work. These actions infringe copyright if you do
 | 
				
			||||||
 | 
					not accept this License. Therefore, by modifying or propagating a
 | 
				
			||||||
 | 
					covered work, you indicate your acceptance of this License to do so.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 10. Automatic Licensing of Downstream Recipients.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each time you convey a covered work, the recipient automatically
 | 
				
			||||||
 | 
					receives a license from the original licensors, to run, modify and
 | 
				
			||||||
 | 
					propagate that work, subject to this License. You are not responsible
 | 
				
			||||||
 | 
					for enforcing compliance by third parties with this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An "entity transaction" is a transaction transferring control of an
 | 
				
			||||||
 | 
					organization, or substantially all assets of one, or subdividing an
 | 
				
			||||||
 | 
					organization, or merging organizations. If propagation of a covered
 | 
				
			||||||
 | 
					work results from an entity transaction, each party to that
 | 
				
			||||||
 | 
					transaction who receives a copy of the work also receives whatever
 | 
				
			||||||
 | 
					licenses to the work the party's predecessor in interest had or could
 | 
				
			||||||
 | 
					give under the previous paragraph, plus a right to possession of the
 | 
				
			||||||
 | 
					Corresponding Source of the work from the predecessor in interest, if
 | 
				
			||||||
 | 
					the predecessor has it or can get it with reasonable efforts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You may not impose any further restrictions on the exercise of the
 | 
				
			||||||
 | 
					rights granted or affirmed under this License. For example, you may
 | 
				
			||||||
 | 
					not impose a license fee, royalty, or other charge for exercise of
 | 
				
			||||||
 | 
					rights granted under this License, and you may not initiate litigation
 | 
				
			||||||
 | 
					(including a cross-claim or counterclaim in a lawsuit) alleging that
 | 
				
			||||||
 | 
					any patent claim is infringed by making, using, selling, offering for
 | 
				
			||||||
 | 
					sale, or importing the Program or any portion of it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 11. Patents.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A "contributor" is a copyright holder who authorizes use under this
 | 
				
			||||||
 | 
					License of the Program or a work on which the Program is based. The
 | 
				
			||||||
 | 
					work thus licensed is called the contributor's "contributor version".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A contributor's "essential patent claims" are all patent claims owned
 | 
				
			||||||
 | 
					or controlled by the contributor, whether already acquired or
 | 
				
			||||||
 | 
					hereafter acquired, that would be infringed by some manner, permitted
 | 
				
			||||||
 | 
					by this License, of making, using, or selling its contributor version,
 | 
				
			||||||
 | 
					but do not include claims that would be infringed only as a
 | 
				
			||||||
 | 
					consequence of further modification of the contributor version. For
 | 
				
			||||||
 | 
					purposes of this definition, "control" includes the right to grant
 | 
				
			||||||
 | 
					patent sublicenses in a manner consistent with the requirements of
 | 
				
			||||||
 | 
					this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each contributor grants you a non-exclusive, worldwide, royalty-free
 | 
				
			||||||
 | 
					patent license under the contributor's essential patent claims, to
 | 
				
			||||||
 | 
					make, use, sell, offer for sale, import and otherwise run, modify and
 | 
				
			||||||
 | 
					propagate the contents of its contributor version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In the following three paragraphs, a "patent license" is any express
 | 
				
			||||||
 | 
					agreement or commitment, however denominated, not to enforce a patent
 | 
				
			||||||
 | 
					(such as an express permission to practice a patent or covenant not to
 | 
				
			||||||
 | 
					sue for patent infringement). To "grant" such a patent license to a
 | 
				
			||||||
 | 
					party means to make such an agreement or commitment not to enforce a
 | 
				
			||||||
 | 
					patent against the party.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you convey a covered work, knowingly relying on a patent license,
 | 
				
			||||||
 | 
					and the Corresponding Source of the work is not available for anyone
 | 
				
			||||||
 | 
					to copy, free of charge and under the terms of this License, through a
 | 
				
			||||||
 | 
					publicly available network server or other readily accessible means,
 | 
				
			||||||
 | 
					then you must either (1) cause the Corresponding Source to be so
 | 
				
			||||||
 | 
					available, or (2) arrange to deprive yourself of the benefit of the
 | 
				
			||||||
 | 
					patent license for this particular work, or (3) arrange, in a manner
 | 
				
			||||||
 | 
					consistent with the requirements of this License, to extend the patent
 | 
				
			||||||
 | 
					license to downstream recipients. "Knowingly relying" means you have
 | 
				
			||||||
 | 
					actual knowledge that, but for the patent license, your conveying the
 | 
				
			||||||
 | 
					covered work in a country, or your recipient's use of the covered work
 | 
				
			||||||
 | 
					in a country, would infringe one or more identifiable patents in that
 | 
				
			||||||
 | 
					country that you have reason to believe are valid.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If, pursuant to or in connection with a single transaction or
 | 
				
			||||||
 | 
					arrangement, you convey, or propagate by procuring conveyance of, a
 | 
				
			||||||
 | 
					covered work, and grant a patent license to some of the parties
 | 
				
			||||||
 | 
					receiving the covered work authorizing them to use, propagate, modify
 | 
				
			||||||
 | 
					or convey a specific copy of the covered work, then the patent license
 | 
				
			||||||
 | 
					you grant is automatically extended to all recipients of the covered
 | 
				
			||||||
 | 
					work and works based on it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A patent license is "discriminatory" if it does not include within the
 | 
				
			||||||
 | 
					scope of its coverage, prohibits the exercise of, or is conditioned on
 | 
				
			||||||
 | 
					the non-exercise of one or more of the rights that are specifically
 | 
				
			||||||
 | 
					granted under this License. You may not convey a covered work if you
 | 
				
			||||||
 | 
					are a party to an arrangement with a third party that is in the
 | 
				
			||||||
 | 
					business of distributing software, under which you make payment to the
 | 
				
			||||||
 | 
					third party based on the extent of your activity of conveying the
 | 
				
			||||||
 | 
					work, and under which the third party grants, to any of the parties
 | 
				
			||||||
 | 
					who would receive the covered work from you, a discriminatory patent
 | 
				
			||||||
 | 
					license (a) in connection with copies of the covered work conveyed by
 | 
				
			||||||
 | 
					you (or copies made from those copies), or (b) primarily for and in
 | 
				
			||||||
 | 
					connection with specific products or compilations that contain the
 | 
				
			||||||
 | 
					covered work, unless you entered into that arrangement, or that patent
 | 
				
			||||||
 | 
					license was granted, prior to 28 March 2007.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Nothing in this License shall be construed as excluding or limiting
 | 
				
			||||||
 | 
					any implied license or other defenses to infringement that may
 | 
				
			||||||
 | 
					otherwise be available to you under applicable patent law.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 12. No Surrender of Others' Freedom.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If conditions are imposed on you (whether by court order, agreement or
 | 
				
			||||||
 | 
					otherwise) that contradict the conditions of this License, they do not
 | 
				
			||||||
 | 
					excuse you from the conditions of this License. If you cannot convey a
 | 
				
			||||||
 | 
					covered work so as to satisfy simultaneously your obligations under
 | 
				
			||||||
 | 
					this License and any other pertinent obligations, then as a
 | 
				
			||||||
 | 
					consequence you may not convey it at all. For example, if you agree to
 | 
				
			||||||
 | 
					terms that obligate you to collect a royalty for further conveying
 | 
				
			||||||
 | 
					from those to whom you convey the Program, the only way you could
 | 
				
			||||||
 | 
					satisfy both those terms and this License would be to refrain entirely
 | 
				
			||||||
 | 
					from conveying the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 13. Remote Network Interaction; Use with the GNU General Public License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Notwithstanding any other provision of this License, if you modify the
 | 
				
			||||||
 | 
					Program, your modified version must prominently offer all users
 | 
				
			||||||
 | 
					interacting with it remotely through a computer network (if your
 | 
				
			||||||
 | 
					version supports such interaction) an opportunity to receive the
 | 
				
			||||||
 | 
					Corresponding Source of your version by providing access to the
 | 
				
			||||||
 | 
					Corresponding Source from a network server at no charge, through some
 | 
				
			||||||
 | 
					standard or customary means of facilitating copying of software. This
 | 
				
			||||||
 | 
					Corresponding Source shall include the Corresponding Source for any
 | 
				
			||||||
 | 
					work covered by version 3 of the GNU General Public License that is
 | 
				
			||||||
 | 
					incorporated pursuant to the following paragraph.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Notwithstanding any other provision of this License, you have
 | 
				
			||||||
 | 
					permission to link or combine any covered work with a work licensed
 | 
				
			||||||
 | 
					under version 3 of the GNU General Public License into a single
 | 
				
			||||||
 | 
					combined work, and to convey the resulting work. The terms of this
 | 
				
			||||||
 | 
					License will continue to apply to the part which is the covered work,
 | 
				
			||||||
 | 
					but the work with which it is combined will remain governed by version
 | 
				
			||||||
 | 
					3 of the GNU General Public License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 14. Revised Versions of this License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Free Software Foundation may publish revised and/or new versions
 | 
				
			||||||
 | 
					of the GNU Affero General Public License from time to time. Such new
 | 
				
			||||||
 | 
					versions will be similar in spirit to the present version, but may
 | 
				
			||||||
 | 
					differ in detail to address new problems or concerns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Each version is given a distinguishing version number. If the Program
 | 
				
			||||||
 | 
					specifies that a certain numbered version of the GNU Affero General
 | 
				
			||||||
 | 
					Public License "or any later version" applies to it, you have the
 | 
				
			||||||
 | 
					option of following the terms and conditions either of that numbered
 | 
				
			||||||
 | 
					version or of any later version published by the Free Software
 | 
				
			||||||
 | 
					Foundation. If the Program does not specify a version number of the
 | 
				
			||||||
 | 
					GNU Affero General Public License, you may choose any version ever
 | 
				
			||||||
 | 
					published by the Free Software Foundation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If the Program specifies that a proxy can decide which future versions
 | 
				
			||||||
 | 
					of the GNU Affero General Public License can be used, that proxy's
 | 
				
			||||||
 | 
					public statement of acceptance of a version permanently authorizes you
 | 
				
			||||||
 | 
					to choose that version for the Program.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Later license versions may give you additional or different
 | 
				
			||||||
 | 
					permissions. However, no additional obligations are imposed on any
 | 
				
			||||||
 | 
					author or copyright holder as a result of your choosing to follow a
 | 
				
			||||||
 | 
					later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 15. Disclaimer of Warranty.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
 | 
				
			||||||
 | 
					APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
 | 
				
			||||||
 | 
					HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
 | 
				
			||||||
 | 
					WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
 | 
				
			||||||
 | 
					LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 | 
				
			||||||
 | 
					A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
 | 
				
			||||||
 | 
					PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
 | 
				
			||||||
 | 
					DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
 | 
				
			||||||
 | 
					CORRECTION.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 16. Limitation of Liability.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
 | 
				
			||||||
 | 
					WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
 | 
				
			||||||
 | 
					CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
 | 
				
			||||||
 | 
					INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
 | 
				
			||||||
 | 
					ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
 | 
				
			||||||
 | 
					NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
 | 
				
			||||||
 | 
					LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
 | 
				
			||||||
 | 
					TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
 | 
				
			||||||
 | 
					PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 17. Interpretation of Sections 15 and 16.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If the disclaimer of warranty and limitation of liability provided
 | 
				
			||||||
 | 
					above cannot be given local legal effect according to their terms,
 | 
				
			||||||
 | 
					reviewing courts shall apply local law that most closely approximates
 | 
				
			||||||
 | 
					an absolute waiver of all civil liability in connection with the
 | 
				
			||||||
 | 
					Program, unless a warranty or assumption of liability accompanies a
 | 
				
			||||||
 | 
					copy of the Program in return for a fee.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					END OF TERMS AND CONDITIONS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### How to Apply These Terms to Your New Programs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you develop a new program, and you want it to be of the greatest
 | 
				
			||||||
 | 
					possible use to the public, the best way to achieve this is to make it
 | 
				
			||||||
 | 
					free software which everyone can redistribute and change under these
 | 
				
			||||||
 | 
					terms.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To do so, attach the following notices to the program. It is safest to
 | 
				
			||||||
 | 
					attach them to the start of each source file to most effectively state
 | 
				
			||||||
 | 
					the exclusion of warranty; and each file should have at least the
 | 
				
			||||||
 | 
					"copyright" line and a pointer to where the full notice is found.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <one line to give the program's name and a brief idea of what it does.>
 | 
				
			||||||
 | 
					        Copyright (C) <year>  <name of author>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This program is free software: you can redistribute it and/or modify
 | 
				
			||||||
 | 
					        it under the terms of the GNU Affero General Public License as
 | 
				
			||||||
 | 
					        published by the Free Software Foundation, either version 3 of the
 | 
				
			||||||
 | 
					        License, or (at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This program is distributed in the hope that it will be useful,
 | 
				
			||||||
 | 
					        but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
 | 
					        GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        You should have received a copy of the GNU Affero General Public License
 | 
				
			||||||
 | 
					        along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Also add information on how to contact you by electronic and paper
 | 
				
			||||||
 | 
					mail.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If your software can interact with users remotely through a computer
 | 
				
			||||||
 | 
					network, you should also make sure that it provides a way for users to
 | 
				
			||||||
 | 
					get its source. For example, if your program is a web application, its
 | 
				
			||||||
 | 
					interface could display a "Source" link that leads users to an archive
 | 
				
			||||||
 | 
					of the code. There are many ways you could offer source, and different
 | 
				
			||||||
 | 
					solutions will be better for different programs; see section 13 for
 | 
				
			||||||
 | 
					the specific requirements.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You should also get your employer (if you work as a programmer) or
 | 
				
			||||||
 | 
					school, if any, to sign a "copyright disclaimer" for the program, if
 | 
				
			||||||
 | 
					necessary. For more information on this, and how to apply and follow
 | 
				
			||||||
 | 
					the GNU AGPL, see <https://www.gnu.org/licenses/>.
 | 
				
			||||||
@@ -5,7 +5,7 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  import Ecto.Query, warn: false
 | 
					  import Ecto.Query, warn: false
 | 
				
			||||||
  alias Memex.{Mailer, Repo}
 | 
					  alias Memex.{Mailer, Repo}
 | 
				
			||||||
  alias Memex.Accounts.{User, UserToken}
 | 
					  alias Memex.Accounts.{Invite, Invites, User, UserToken}
 | 
				
			||||||
  alias Ecto.{Changeset, Multi}
 | 
					  alias Ecto.{Changeset, Multi}
 | 
				
			||||||
  alias Oban.Job
 | 
					  alias Oban.Job
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -24,14 +24,16 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_user_by_email(email :: String.t()) :: User.t() | nil
 | 
					  @spec get_user_by_email(email :: String.t()) :: User.t() | nil
 | 
				
			||||||
  def get_user_by_email(email) when is_binary(email), do: Repo.get_by(User, email: email)
 | 
					  def get_user_by_email(email) when is_binary(email) do
 | 
				
			||||||
 | 
					    Repo.get_by(User, email: email)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Gets a user by email and password.
 | 
					  Gets a user by email and password.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_user_by_email_and_password("foo@example.com", "correct_password")
 | 
					      iex> get_user_by_email_and_password("foo@example.com", "valid_password")
 | 
				
			||||||
      %User{}
 | 
					      %User{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
 | 
					      iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
 | 
				
			||||||
@@ -53,28 +55,30 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_user!(123)
 | 
					      iex> get_user!(user_id)
 | 
				
			||||||
      %User{}
 | 
					      user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_user!(456)
 | 
					      iex> get_user!()
 | 
				
			||||||
      ** (Ecto.NoResultsError)
 | 
					      ** (Ecto.NoResultsError)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec get_user!(User.t()) :: User.t()
 | 
					  @spec get_user!(User.id()) :: User.t()
 | 
				
			||||||
  def get_user!(id), do: Repo.get!(User, id)
 | 
					  def get_user!(id) do
 | 
				
			||||||
 | 
					    Repo.get!(User, id)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns all users grouped by role.
 | 
					  Returns all users grouped by role.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> list_users_by_role(%User{id: 123, role: :admin})
 | 
					      iex> list_all_users_by_role(user1)
 | 
				
			||||||
      [admin: [%User{}], user: [%User{}, %User{}]]
 | 
					      %{admin: [%User{role: :admin}], user: [%User{role: :user}]}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_all_users_by_role(User.t()) :: %{String.t() => [User.t()]}
 | 
					  @spec list_all_users_by_role(User.t()) :: %{User.role() => [User.t()]}
 | 
				
			||||||
  def list_all_users_by_role(%User{role: :admin}) do
 | 
					  def list_all_users_by_role(%User{role: :admin}) do
 | 
				
			||||||
    Repo.all(from u in User, order_by: u.email) |> Enum.group_by(fn user -> user.role end)
 | 
					    Repo.all(from u in User, order_by: u.email) |> Enum.group_by(fn %{role: role} -> role end)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -82,13 +86,12 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> list_users_by_role(%User{id: 123, role: :admin})
 | 
					      iex> list_users_by_role(:admin)
 | 
				
			||||||
      [%User{}]
 | 
					      [%User{role: :admin}]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec list_users_by_role(User.role()) :: [User.t()]
 | 
					  @spec list_users_by_role(:admin) :: [User.t()]
 | 
				
			||||||
  def list_users_by_role(role) do
 | 
					  def list_users_by_role(:admin = role) do
 | 
				
			||||||
    role = role |> to_string()
 | 
					 | 
				
			||||||
    Repo.all(from u in User, where: u.role == ^role, order_by: u.email)
 | 
					    Repo.all(from u in User, where: u.role == ^role, order_by: u.email)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -99,26 +102,36 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> register_user(%{field: value})
 | 
					      iex> register_user(%{email: "foo@example.com", password: "valid_password"})
 | 
				
			||||||
      {:ok, %User{}}
 | 
					      {:ok, %User{email: "foo@example.com"}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> register_user(%{field: bad_value})
 | 
					      iex> register_user(%{email: "foo@example"})
 | 
				
			||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec register_user(attrs :: map()) :: {:ok, User.t()} | {:error, User.changeset()}
 | 
					  @spec register_user(attrs :: map()) ::
 | 
				
			||||||
  def register_user(attrs) do
 | 
					          {:ok, User.t()} | {:error, :invalid_token | User.changeset()}
 | 
				
			||||||
 | 
					  @spec register_user(attrs :: map(), Invite.token() | nil) ::
 | 
				
			||||||
 | 
					          {:ok, User.t()} | {:error, :invalid_token | User.changeset()}
 | 
				
			||||||
 | 
					  def register_user(attrs, invite_token \\ nil) do
 | 
				
			||||||
    Multi.new()
 | 
					    Multi.new()
 | 
				
			||||||
    |> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true))
 | 
					    |> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true))
 | 
				
			||||||
    |> Multi.insert(:add_user, fn %{users_count: count} ->
 | 
					    |> Multi.run(:use_invite, fn _changes_so_far, _repo ->
 | 
				
			||||||
 | 
					      if allow_registration?() and invite_token |> is_nil() do
 | 
				
			||||||
 | 
					        {:ok, nil}
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        Invites.use_invite(invite_token)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    |> Multi.insert(:add_user, fn %{users_count: count, use_invite: invite} ->
 | 
				
			||||||
      # if no registered users, make first user an admin
 | 
					      # if no registered users, make first user an admin
 | 
				
			||||||
      role = if count == 0, do: "admin", else: "user"
 | 
					      role = if count == 0, do: :admin, else: :user
 | 
				
			||||||
 | 
					      User.registration_changeset(attrs, invite) |> User.role_changeset(role)
 | 
				
			||||||
      User.registration_changeset(attrs) |> User.role_changeset(role)
 | 
					 | 
				
			||||||
    end)
 | 
					    end)
 | 
				
			||||||
    |> Repo.transaction()
 | 
					    |> Repo.transaction()
 | 
				
			||||||
    |> case do
 | 
					    |> case do
 | 
				
			||||||
      {:ok, %{add_user: user}} -> {:ok, user}
 | 
					      {:ok, %{add_user: user}} -> {:ok, user}
 | 
				
			||||||
 | 
					      {:error, :use_invite, :invalid_token, _changes_so_far} -> {:error, :invalid_token}
 | 
				
			||||||
      {:error, :add_user, changeset, _changes_so_far} -> {:error, changeset}
 | 
					      {:error, :add_user, changeset, _changes_so_far} -> {:error, changeset}
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -128,14 +141,18 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> change_user_registration(user)
 | 
					      iex> change_user_registration()
 | 
				
			||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> change_user_registration(%{password: "hi"}
 | 
				
			||||||
 | 
					      %Changeset{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec change_user_registration() :: User.changeset()
 | 
					  @spec change_user_registration() :: User.changeset()
 | 
				
			||||||
  @spec change_user_registration(attrs :: map()) :: User.changeset()
 | 
					  @spec change_user_registration(attrs :: map()) :: User.changeset()
 | 
				
			||||||
  def change_user_registration(attrs \\ %{}),
 | 
					  def change_user_registration(attrs \\ %{}) do
 | 
				
			||||||
    do: User.registration_changeset(attrs, hash_password: false)
 | 
					    User.registration_changeset(attrs, nil, hash_password: false)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Settings
 | 
					  ## Settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -144,24 +161,29 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> change_user_email(user)
 | 
					      iex> change_user_email(%User{email: "foo@example.com"})
 | 
				
			||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					  @spec change_user_email(User.t()) :: User.changeset()
 | 
				
			||||||
  @spec change_user_email(User.t(), attrs :: map()) :: User.changeset()
 | 
					  @spec change_user_email(User.t(), attrs :: map()) :: User.changeset()
 | 
				
			||||||
  def change_user_email(user, attrs \\ %{}), do: User.email_changeset(user, attrs)
 | 
					  def change_user_email(user, attrs \\ %{}) do
 | 
				
			||||||
 | 
					    User.email_changeset(user, attrs)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns an `%Changeset{}` for changing the user role.
 | 
					  Returns an `%Changeset{}` for changing the user role.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> change_user_role(user)
 | 
					      iex> change_user_role(%User{}, :user)
 | 
				
			||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec change_user_role(User.t(), User.role()) :: User.changeset()
 | 
					  @spec change_user_role(User.t(), User.role()) :: User.changeset()
 | 
				
			||||||
  def change_user_role(user, role), do: User.role_changeset(user, role)
 | 
					  def change_user_role(user, role) do
 | 
				
			||||||
 | 
					    User.role_changeset(user, role)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Emulates that the email will change without actually changing
 | 
					  Emulates that the email will change without actually changing
 | 
				
			||||||
@@ -169,14 +191,14 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> apply_user_email(user, "valid password", %{email: ...})
 | 
					      iex> apply_user_email(user, "valid_password", %{email: "new_email@account.com"})
 | 
				
			||||||
      {:ok, %User{}}
 | 
					      {:ok, %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> apply_user_email(user, "invalid password", %{email: ...})
 | 
					      iex> apply_user_email(user, "invalid password", %{email: "new_email@account"})
 | 
				
			||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec apply_user_email(User.t(), password :: String.t(), attrs :: map()) ::
 | 
					  @spec apply_user_email(User.t(), email :: String.t(), attrs :: map()) ::
 | 
				
			||||||
          {:ok, User.t()} | {:error, User.changeset()}
 | 
					          {:ok, User.t()} | {:error, User.changeset()}
 | 
				
			||||||
  def apply_user_email(user, password, attrs) do
 | 
					  def apply_user_email(user, password, attrs) do
 | 
				
			||||||
    user
 | 
					    user
 | 
				
			||||||
@@ -200,7 +222,7 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
         {:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
 | 
					         {:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
 | 
				
			||||||
      :ok
 | 
					      :ok
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      _ -> :error
 | 
					      _error_tuple -> :error
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -218,8 +240,8 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> deliver_update_email_instructions(user, current_email, &Routes.user_update_email_url(conn, :edit, &1))
 | 
					      iex> deliver_update_email_instructions(user, "new_foo@example.com", fn _token -> "example url" end)
 | 
				
			||||||
      {:ok, %{to: ..., body: ...}}
 | 
					      %Oban.Job{args: %{email: :update_email, user_id: ^user_id, attrs: %{url: "example url"}}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec deliver_update_email_instructions(User.t(), current_email :: String.t(), function) ::
 | 
					  @spec deliver_update_email_instructions(User.t(), current_email :: String.t(), function) ::
 | 
				
			||||||
@@ -236,27 +258,31 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> change_user_password(user)
 | 
					      iex> change_user_password(%User{})
 | 
				
			||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec change_user_password(User.t(), attrs :: map()) :: User.changeset()
 | 
					  @spec change_user_password(User.t(), attrs :: map()) :: User.changeset()
 | 
				
			||||||
  def change_user_password(user, attrs \\ %{}),
 | 
					  def change_user_password(user, attrs \\ %{}) do
 | 
				
			||||||
    do: User.password_changeset(user, attrs, hash_password: false)
 | 
					    User.password_changeset(user, attrs, hash_password: false)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Updates the user password.
 | 
					  Updates the user password.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> update_user_password(user, "valid password", %{password: ...})
 | 
					      iex> reset_user_password(user, %{
 | 
				
			||||||
 | 
					      ...>   password: "new password",
 | 
				
			||||||
 | 
					      ...>   password_confirmation: "new password"
 | 
				
			||||||
 | 
					      ...> })
 | 
				
			||||||
      {:ok, %User{}}
 | 
					      {:ok, %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> update_user_password(user, "invalid password", %{password: ...})
 | 
					      iex> update_user_password(user, "invalid password", %{password: "123"})
 | 
				
			||||||
      {:error, %Changeset{}}
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_user_password(User.t(), password :: String.t(), attrs :: map()) ::
 | 
					  @spec update_user_password(User.t(), String.t(), attrs :: map()) ::
 | 
				
			||||||
          {:ok, User.t()} | {:error, User.changeset()}
 | 
					          {:ok, User.t()} | {:error, User.changeset()}
 | 
				
			||||||
  def update_user_password(user, password, attrs) do
 | 
					  def update_user_password(user, password, attrs) do
 | 
				
			||||||
    changeset =
 | 
					    changeset =
 | 
				
			||||||
@@ -270,54 +296,59 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
    |> Repo.transaction()
 | 
					    |> Repo.transaction()
 | 
				
			||||||
    |> case do
 | 
					    |> case do
 | 
				
			||||||
      {:ok, %{user: user}} -> {:ok, user}
 | 
					      {:ok, %{user: user}} -> {:ok, user}
 | 
				
			||||||
      {:error, :user, changeset, _} -> {:error, changeset}
 | 
					      {:error, :user, changeset, _changes_so_far} -> {:error, changeset}
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Returns an `%Changeset{}` for changing the user locale.
 | 
					  Returns an `Ecto.Changeset.t()` for changing the user locale.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> change_user_locale(user)
 | 
					      iex> change_user_locale(%User{})
 | 
				
			||||||
      %Changeset{data: %User{}}
 | 
					      %Changeset{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec change_user_locale(User.t()) :: User.changeset()
 | 
					  @spec change_user_locale(User.t()) :: User.changeset()
 | 
				
			||||||
  def change_user_locale(%{locale: locale} = user), do: User.locale_changeset(user, locale)
 | 
					  def change_user_locale(%{locale: locale} = user) do
 | 
				
			||||||
 | 
					    User.locale_changeset(user, locale)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Updates the user locale.
 | 
					  Updates the user locale.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> update_user_locale(user, "valid locale")
 | 
					      iex> update_user_locale(user, "en_US")
 | 
				
			||||||
      {:ok, %User{}}
 | 
					      {:ok, %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> update_user_password(user, "invalid locale")
 | 
					 | 
				
			||||||
      {:error, %Changeset{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec update_user_locale(User.t(), locale :: String.t()) ::
 | 
					  @spec update_user_locale(User.t(), locale :: String.t()) ::
 | 
				
			||||||
          {:ok, User.t()} | {:error, User.changeset()}
 | 
					          {:ok, User.t()} | {:error, User.changeset()}
 | 
				
			||||||
  def update_user_locale(user, locale),
 | 
					  def update_user_locale(user, locale) do
 | 
				
			||||||
    do: user |> User.locale_changeset(locale) |> Repo.update()
 | 
					    user |> User.locale_changeset(locale) |> Repo.update()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Deletes a user. must be performed by an admin or the same user!
 | 
					  Deletes a user. must be performed by an admin or the same user!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> delete_user!(user_to_delete, %User{id: 123, role: :admin})
 | 
					      iex> delete_user!(user, %User{id: 123, role: :admin})
 | 
				
			||||||
      %User{}
 | 
					      %User{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> delete_user!(%User{id: 123}, %User{id: 123})
 | 
					      iex> delete_user!(user, user)
 | 
				
			||||||
      %User{}
 | 
					      %User{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_user!(user_to_delete :: User.t(), User.t()) :: User.t()
 | 
					  @spec delete_user!(user_to_delete :: User.t(), User.t()) :: User.t()
 | 
				
			||||||
  def delete_user!(user, %User{role: :admin}), do: user |> Repo.delete!()
 | 
					  def delete_user!(user, %User{role: :admin}) do
 | 
				
			||||||
  def delete_user!(%User{id: user_id} = user, %User{id: user_id}), do: user |> Repo.delete!()
 | 
					    user |> Repo.delete!()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def delete_user!(%User{id: user_id} = user, %User{id: user_id}) do
 | 
				
			||||||
 | 
					    user |> Repo.delete!()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ## Session
 | 
					  ## Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -343,9 +374,9 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Deletes the signed token with the given context.
 | 
					  Deletes the signed token with the given context.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec delete_session_token(token :: String.t()) :: :ok
 | 
					  @spec delete_user_session_token(token :: String.t()) :: :ok
 | 
				
			||||||
  def delete_session_token(token) do
 | 
					  def delete_user_session_token(token) do
 | 
				
			||||||
    Repo.delete_all(UserToken.token_and_context_query(token, "session"))
 | 
					    UserToken.token_and_context_query(token, "session") |> Repo.delete_all()
 | 
				
			||||||
    :ok
 | 
					    :ok
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -354,20 +385,49 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec allow_registration?() :: boolean()
 | 
					  @spec allow_registration?() :: boolean()
 | 
				
			||||||
  def allow_registration? do
 | 
					  def allow_registration? do
 | 
				
			||||||
    Application.get_env(:memex, MemexWeb.Endpoint)[:registration] == "public" or
 | 
					    Application.get_env(:memex, Memex.Accounts)[:registration] == "public" or
 | 
				
			||||||
      list_users_by_role(:admin) |> Enum.empty?()
 | 
					      list_users_by_role(:admin) |> Enum.empty?()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns an atom representing the current configured registration mode
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec registration_mode() :: :public | :invite_only
 | 
				
			||||||
 | 
					  def registration_mode do
 | 
				
			||||||
 | 
					    case Application.get_env(:memex, Memex.Accounts)[:registration] do
 | 
				
			||||||
 | 
					      "public" -> :public
 | 
				
			||||||
 | 
					      _other -> :invite_only
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Checks if user is an admin
 | 
					  Checks if user is an admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> is_admin?(%User{role: :admin})
 | 
				
			||||||
 | 
					      true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> is_admin?(%User{})
 | 
				
			||||||
 | 
					      false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec is_admin?(User.t()) :: boolean()
 | 
					  @spec is_admin?(User.t()) :: boolean()
 | 
				
			||||||
  def is_admin?(%User{id: user_id}) do
 | 
					  def is_admin?(%User{id: user_id}) do
 | 
				
			||||||
    Repo.exists?(from u in User, where: u.id == ^user_id and u.role == :admin)
 | 
					    Repo.exists?(from u in User, where: u.id == ^user_id, where: u.role == :admin)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Checks to see if user has the admin role
 | 
					  Checks to see if user has the admin role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> is_already_admin?(%User{role: :admin})
 | 
				
			||||||
 | 
					      true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> is_already_admin?(%User{})
 | 
				
			||||||
 | 
					      false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec is_already_admin?(User.t() | nil) :: boolean()
 | 
					  @spec is_already_admin?(User.t() | nil) :: boolean()
 | 
				
			||||||
  def is_already_admin?(%User{role: :admin}), do: true
 | 
					  def is_already_admin?(%User{role: :admin}), do: true
 | 
				
			||||||
@@ -380,10 +440,10 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> deliver_user_confirmation_instructions(user, &Routes.user_confirmation_url(conn, :confirm, &1))
 | 
					      iex> deliver_user_confirmation_instructions(user, fn _token -> "example url" end)
 | 
				
			||||||
      {:ok, %{to: ..., body: ...}}
 | 
					      %Oban.Job{args: %{email: :welcome, user_id: ^user_id, attrs: %{url: "example url"}}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> deliver_user_confirmation_instructions(confirmed_user, &Routes.user_confirmation_url(conn, :confirm, &1))
 | 
					      iex> deliver_user_confirmation_instructions(user, fn _token -> "example url" end)
 | 
				
			||||||
      {:error, :already_confirmed}
 | 
					      {:error, :already_confirmed}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
@@ -405,14 +465,14 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
  If the token matches, the user account is marked as confirmed
 | 
					  If the token matches, the user account is marked as confirmed
 | 
				
			||||||
  and the token is deleted.
 | 
					  and the token is deleted.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec confirm_user(token :: String.t()) :: {:ok, User.t()} | atom()
 | 
					  @spec confirm_user(token :: String.t()) :: {:ok, User.t()} | :error
 | 
				
			||||||
  def confirm_user(token) do
 | 
					  def confirm_user(token) do
 | 
				
			||||||
    with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
 | 
					    with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
 | 
				
			||||||
         %User{} = user <- Repo.one(query),
 | 
					         %User{} = user <- Repo.one(query),
 | 
				
			||||||
         {:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
 | 
					         {:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
 | 
				
			||||||
      {:ok, user}
 | 
					      {:ok, user}
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      _ -> :error
 | 
					      _error_tuple -> :error
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -430,8 +490,8 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> deliver_user_reset_password_instructions(user, &Routes.user_reset_password_url(conn, :edit, &1))
 | 
					      iex> deliver_user_reset_password_instructions(user, fn _token -> "example url" end)
 | 
				
			||||||
      {:ok, %{to: ..., body: ...}}
 | 
					      %Oban.Job{args: %{email: :reset_password, user_id: ^user_id, attrs: %{url: "example url"}}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec deliver_user_reset_password_instructions(User.t(), function()) :: Job.t()
 | 
					  @spec deliver_user_reset_password_instructions(User.t(), function()) :: Job.t()
 | 
				
			||||||
@@ -447,7 +507,7 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_user_by_reset_password_token("validtoken")
 | 
					      iex> get_user_by_reset_password_token(encoded_token)
 | 
				
			||||||
      %User{}
 | 
					      %User{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> get_user_by_reset_password_token("invalidtoken")
 | 
					      iex> get_user_by_reset_password_token("invalidtoken")
 | 
				
			||||||
@@ -460,7 +520,7 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
         %User{} = user <- Repo.one(query) do
 | 
					         %User{} = user <- Repo.one(query) do
 | 
				
			||||||
      user
 | 
					      user
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      _ -> nil
 | 
					      _error_tuple -> nil
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -469,7 +529,10 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  ## Examples
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"})
 | 
					      iex> reset_user_password(user, %{
 | 
				
			||||||
 | 
					      ...>   password: "new password",
 | 
				
			||||||
 | 
					      ...>   password_confirmation: "new password"
 | 
				
			||||||
 | 
					      ...> })
 | 
				
			||||||
      {:ok, %User{}}
 | 
					      {:ok, %User{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
 | 
					      iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
 | 
				
			||||||
@@ -485,7 +548,7 @@ defmodule Memex.Accounts do
 | 
				
			|||||||
    |> Repo.transaction()
 | 
					    |> Repo.transaction()
 | 
				
			||||||
    |> case do
 | 
					    |> case do
 | 
				
			||||||
      {:ok, %{user: user}} -> {:ok, user}
 | 
					      {:ok, %{user: user}} -> {:ok, user}
 | 
				
			||||||
      {:error, :user, changeset, _} -> {:error, changeset}
 | 
					      {:error, :user, changeset, _changes_so_far} -> {:error, changeset}
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,14 +3,15 @@ defmodule Memex.Email do
 | 
				
			|||||||
  Emails that can be sent using Swoosh.
 | 
					  Emails that can be sent using Swoosh.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  You can find the base email templates at
 | 
					  You can find the base email templates at
 | 
				
			||||||
  `lib/memex_web/templates/layout/email.html.heex` for html emails and
 | 
					  `lib/memex_web/components/layouts/email_html.html.heex` for html emails and
 | 
				
			||||||
  `lib/memex_web/templates/layout/email.txt.heex` for text emails.
 | 
					  `lib/memex_web/components/layouts/email_text.txt.eex` for text emails.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use Phoenix.Swoosh, view: MemexWeb.EmailView, layout: {MemexWeb.LayoutView, :email}
 | 
					  import Swoosh.Email
 | 
				
			||||||
  import MemexWeb.Gettext
 | 
					  import MemexWeb.Gettext
 | 
				
			||||||
 | 
					  import Phoenix.Template
 | 
				
			||||||
  alias Memex.Accounts.User
 | 
					  alias Memex.Accounts.User
 | 
				
			||||||
  alias MemexWeb.EmailView
 | 
					  alias MemexWeb.{EmailHTML, Layouts}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @typedoc """
 | 
					  @typedoc """
 | 
				
			||||||
  Represents an HTML and text body email that can be sent
 | 
					  Represents an HTML and text body email that can be sent
 | 
				
			||||||
@@ -27,22 +28,29 @@ defmodule Memex.Email do
 | 
				
			|||||||
  @spec generate_email(key :: String.t(), User.t(), attrs :: map()) :: t()
 | 
					  @spec generate_email(key :: String.t(), User.t(), attrs :: map()) :: t()
 | 
				
			||||||
  def generate_email("welcome", user, %{"url" => url}) do
 | 
					  def generate_email("welcome", user, %{"url" => url}) do
 | 
				
			||||||
    user
 | 
					    user
 | 
				
			||||||
    |> base_email(dgettext("emails", "Confirm your Memex account"))
 | 
					    |> base_email(dgettext("emails", "confirm your memEx account"))
 | 
				
			||||||
    |> render_body("confirm_email.html", %{user: user, url: url})
 | 
					    |> render_body(:confirm_email, %{user: user, url: url})
 | 
				
			||||||
    |> text_body(EmailView.render("confirm_email.txt", %{user: user, url: url}))
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def generate_email("reset_password", user, %{"url" => url}) do
 | 
					  def generate_email("reset_password", user, %{"url" => url}) do
 | 
				
			||||||
    user
 | 
					    user
 | 
				
			||||||
    |> base_email(dgettext("emails", "Reset your Memex password"))
 | 
					    |> base_email(dgettext("emails", "reset your memEx password"))
 | 
				
			||||||
    |> render_body("reset_password.html", %{user: user, url: url})
 | 
					    |> render_body(:reset_password, %{user: user, url: url})
 | 
				
			||||||
    |> text_body(EmailView.render("reset_password.txt", %{user: user, url: url}))
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def generate_email("update_email", user, %{"url" => url}) do
 | 
					  def generate_email("update_email", user, %{"url" => url}) do
 | 
				
			||||||
    user
 | 
					    user
 | 
				
			||||||
    |> base_email(dgettext("emails", "Update your Memex email"))
 | 
					    |> base_email(dgettext("emails", "update your memEx email"))
 | 
				
			||||||
    |> render_body("update_email.html", %{user: user, url: url})
 | 
					    |> render_body(:update_email, %{user: user, url: url})
 | 
				
			||||||
    |> text_body(EmailView.render("update_email.txt", %{user: user, url: url}))
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp render_body(email, template, assigns) do
 | 
				
			||||||
 | 
					    html_heex = apply(EmailHTML, String.to_existing_atom("#{template}_html"), [assigns])
 | 
				
			||||||
 | 
					    html = render_to_string(Layouts, "email_html", "html", email: email, inner_content: html_heex)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    text_heex = apply(EmailHTML, String.to_existing_atom("#{template}_text"), [assigns])
 | 
				
			||||||
 | 
					    text = render_to_string(Layouts, "email_text", "text", email: email, inner_content: text_heex)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    email |> html_body(html) |> text_body(text)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
defmodule Memex.Invites.Invite do
 | 
					defmodule Memex.Accounts.Invite do
 | 
				
			||||||
  @moduledoc """
 | 
					  @moduledoc """
 | 
				
			||||||
  An invite, created by an admin to allow someone to join their instance. An
 | 
					  An invite, created by an admin to allow someone to join their instance. An
 | 
				
			||||||
  invite can be enabled or disabled, and can have an optional number of uses if
 | 
					  invite can be enabled or disabled, and can have an optional number of uses if
 | 
				
			||||||
@@ -7,8 +7,8 @@ defmodule Memex.Invites.Invite do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use Ecto.Schema
 | 
					  use Ecto.Schema
 | 
				
			||||||
  import Ecto.Changeset
 | 
					  import Ecto.Changeset
 | 
				
			||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Association, Changeset, UUID}
 | 
				
			||||||
  alias Memex.{Accounts.User, Invites.Invite}
 | 
					  alias Memex.Accounts.User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
@@ -18,40 +18,46 @@ defmodule Memex.Invites.Invite do
 | 
				
			|||||||
    field :uses_left, :integer, default: nil
 | 
					    field :uses_left, :integer, default: nil
 | 
				
			||||||
    field :disabled_at, :naive_datetime
 | 
					    field :disabled_at, :naive_datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    belongs_to :user, User
 | 
					    belongs_to :created_by, User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    has_many :users, User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    timestamps()
 | 
					    timestamps()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @type t :: %Invite{
 | 
					  @type t :: %__MODULE__{
 | 
				
			||||||
          id: id(),
 | 
					          id: id(),
 | 
				
			||||||
          name: String.t(),
 | 
					          name: String.t(),
 | 
				
			||||||
          token: String.t(),
 | 
					          token: token(),
 | 
				
			||||||
          uses_left: integer() | nil,
 | 
					          uses_left: integer() | nil,
 | 
				
			||||||
          disabled_at: NaiveDateTime.t(),
 | 
					          disabled_at: NaiveDateTime.t(),
 | 
				
			||||||
          user: User.t(),
 | 
					          created_by: User.t() | nil | Association.NotLoaded.t(),
 | 
				
			||||||
          user_id: User.id(),
 | 
					          created_by_id: User.id() | nil,
 | 
				
			||||||
 | 
					          users: [User.t()] | Association.NotLoaded.t(),
 | 
				
			||||||
          inserted_at: NaiveDateTime.t(),
 | 
					          inserted_at: NaiveDateTime.t(),
 | 
				
			||||||
          updated_at: NaiveDateTime.t()
 | 
					          updated_at: NaiveDateTime.t()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_invite :: %Invite{}
 | 
					  @type new_invite :: %__MODULE__{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type changeset :: Changeset.t(t() | new_invite())
 | 
				
			||||||
 | 
					  @type token :: String.t()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec create_changeset(new_invite(), attrs :: map()) :: Changeset.t(new_invite())
 | 
					  @spec create_changeset(User.t(), token(), attrs :: map()) :: changeset()
 | 
				
			||||||
  def create_changeset(invite, attrs) do
 | 
					  def create_changeset(%User{id: user_id}, token, attrs) do
 | 
				
			||||||
    invite
 | 
					    %__MODULE__{}
 | 
				
			||||||
    |> cast(attrs, [:name, :token, :uses_left, :disabled_at, :user_id])
 | 
					    |> change(token: token, created_by_id: user_id)
 | 
				
			||||||
    |> validate_required([:name, :token, :user_id])
 | 
					    |> cast(attrs, [:name, :uses_left, :disabled_at])
 | 
				
			||||||
 | 
					    |> validate_required([:name, :token, :created_by_id])
 | 
				
			||||||
    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
					    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc false
 | 
					  @doc false
 | 
				
			||||||
  @spec update_changeset(t() | new_invite(), attrs :: map()) :: Changeset.t(t() | new_invite())
 | 
					  @spec update_changeset(t() | new_invite(), attrs :: map()) :: changeset()
 | 
				
			||||||
  def update_changeset(invite, attrs) do
 | 
					  def update_changeset(invite, attrs) do
 | 
				
			||||||
    invite
 | 
					    invite
 | 
				
			||||||
    |> cast(attrs, [:name, :uses_left, :disabled_at])
 | 
					    |> cast(attrs, [:name, :uses_left, :disabled_at])
 | 
				
			||||||
    |> validate_required([:name, :token, :user_id])
 | 
					    |> validate_required([:name])
 | 
				
			||||||
    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
					    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
							
								
								
									
										208
									
								
								lib/memex/accounts/invites.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								lib/memex/accounts/invites.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,208 @@
 | 
				
			|||||||
 | 
					defmodule Memex.Accounts.Invites do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  The Invites context.
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  import Ecto.Query, warn: false
 | 
				
			||||||
 | 
					  alias Ecto.Multi
 | 
				
			||||||
 | 
					  alias Memex.Accounts.{Invite, User}
 | 
				
			||||||
 | 
					  alias Memex.Repo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @invite_token_length 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns the list of invites.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> list_invites(%User{id: 123, role: :admin})
 | 
				
			||||||
 | 
					      [%Invite{}, ...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec list_invites(User.t()) :: [Invite.t()]
 | 
				
			||||||
 | 
					  def list_invites(%User{role: :admin}) do
 | 
				
			||||||
 | 
					    Repo.all(from i in Invite, order_by: i.name)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Gets a single invite for a user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Raises `Ecto.NoResultsError` if the Invite does not exist.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> get_invite!(123, %User{id: 123, role: :admin})
 | 
				
			||||||
 | 
					      %Invite{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      > get_invite!(456, %User{id: 123, role: :admin})
 | 
				
			||||||
 | 
					      ** (Ecto.NoResultsError)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec get_invite!(Invite.id(), User.t()) :: Invite.t()
 | 
				
			||||||
 | 
					  def get_invite!(id, %User{role: :admin}) do
 | 
				
			||||||
 | 
					    Repo.get!(Invite, id)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Returns if an invite token is still valid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> valid_invite_token?("valid_token")
 | 
				
			||||||
 | 
					      %Invite{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> valid_invite_token?("invalid_token")
 | 
				
			||||||
 | 
					      nil
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec valid_invite_token?(Invite.token() | nil) :: boolean()
 | 
				
			||||||
 | 
					  def valid_invite_token?(token) when token in [nil, ""], do: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def valid_invite_token?(token) do
 | 
				
			||||||
 | 
					    Repo.exists?(
 | 
				
			||||||
 | 
					      from i in Invite,
 | 
				
			||||||
 | 
					        where: i.token == ^token,
 | 
				
			||||||
 | 
					        where: i.disabled_at |> is_nil()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Uses invite by decrementing uses_left, or marks invite invalid if it's been
 | 
				
			||||||
 | 
					  completely used.
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec use_invite(Invite.token()) :: {:ok, Invite.t()} | {:error, :invalid_token}
 | 
				
			||||||
 | 
					  def use_invite(invite_token) do
 | 
				
			||||||
 | 
					    Multi.new()
 | 
				
			||||||
 | 
					    |> Multi.run(:invite, fn _changes_so_far, _repo ->
 | 
				
			||||||
 | 
					      invite_token |> get_invite_by_token()
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    |> Multi.update(:decrement_invite, fn %{invite: invite} ->
 | 
				
			||||||
 | 
					      decrement_invite_changeset(invite)
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    |> Repo.transaction()
 | 
				
			||||||
 | 
					    |> case do
 | 
				
			||||||
 | 
					      {:ok, %{decrement_invite: invite}} -> {:ok, invite}
 | 
				
			||||||
 | 
					      {:error, :invite, :invalid_token, _changes_so_far} -> {:error, :invalid_token}
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec get_invite_by_token(Invite.token() | nil) :: {:ok, Invite.t()} | {:error, :invalid_token}
 | 
				
			||||||
 | 
					  defp get_invite_by_token(token) when token in [nil, ""], do: {:error, :invalid_token}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_invite_by_token(token) do
 | 
				
			||||||
 | 
					    Repo.one(
 | 
				
			||||||
 | 
					      from i in Invite,
 | 
				
			||||||
 | 
					        where: i.token == ^token,
 | 
				
			||||||
 | 
					        where: i.disabled_at |> is_nil()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> case do
 | 
				
			||||||
 | 
					      nil -> {:error, :invalid_token}
 | 
				
			||||||
 | 
					      invite -> {:ok, invite}
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec get_use_count(Invite.t(), User.t()) :: non_neg_integer() | nil
 | 
				
			||||||
 | 
					  def get_use_count(%Invite{id: invite_id} = invite, user) do
 | 
				
			||||||
 | 
					    [invite] |> get_use_counts(user) |> Map.get(invite_id)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec get_use_counts([Invite.t()], User.t()) ::
 | 
				
			||||||
 | 
					          %{optional(Invite.id()) => non_neg_integer()}
 | 
				
			||||||
 | 
					  def get_use_counts(invites, %User{role: :admin}) do
 | 
				
			||||||
 | 
					    invite_ids = invites |> Enum.map(fn %{id: invite_id} -> invite_id end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Repo.all(
 | 
				
			||||||
 | 
					      from u in User,
 | 
				
			||||||
 | 
					        where: u.invite_id in ^invite_ids,
 | 
				
			||||||
 | 
					        group_by: u.invite_id,
 | 
				
			||||||
 | 
					        select: {u.invite_id, count(u.id)}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    |> Map.new()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec decrement_invite_changeset(Invite.t()) :: Invite.changeset()
 | 
				
			||||||
 | 
					  defp decrement_invite_changeset(%Invite{uses_left: nil} = invite) do
 | 
				
			||||||
 | 
					    invite |> Invite.update_changeset(%{})
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp decrement_invite_changeset(%Invite{uses_left: 1} = invite) do
 | 
				
			||||||
 | 
					    now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
 | 
				
			||||||
 | 
					    invite |> Invite.update_changeset(%{uses_left: 0, disabled_at: now})
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp decrement_invite_changeset(%Invite{uses_left: uses_left} = invite) do
 | 
				
			||||||
 | 
					    invite |> Invite.update_changeset(%{uses_left: uses_left - 1})
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Creates a invite.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> create_invite(%User{id: 123, role: :admin}, %{field: value})
 | 
				
			||||||
 | 
					      {:ok, %Invite{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> create_invite(%User{id: 123, role: :admin}, %{field: bad_value})
 | 
				
			||||||
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec create_invite(User.t(), attrs :: map()) ::
 | 
				
			||||||
 | 
					          {:ok, Invite.t()} | {:error, Invite.changeset()}
 | 
				
			||||||
 | 
					  def create_invite(%User{role: :admin} = user, attrs) do
 | 
				
			||||||
 | 
					    token =
 | 
				
			||||||
 | 
					      :crypto.strong_rand_bytes(@invite_token_length)
 | 
				
			||||||
 | 
					      |> Base.url_encode64()
 | 
				
			||||||
 | 
					      |> binary_part(0, @invite_token_length)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Invite.create_changeset(user, token, attrs) |> Repo.insert()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Updates a invite.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> update_invite(invite, %{field: new_value}, %User{id: 123, role: :admin})
 | 
				
			||||||
 | 
					      {:ok, %Invite{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> update_invite(invite, %{field: bad_value}, %User{id: 123, role: :admin})
 | 
				
			||||||
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec update_invite(Invite.t(), attrs :: map(), User.t()) ::
 | 
				
			||||||
 | 
					          {:ok, Invite.t()} | {:error, Invite.changeset()}
 | 
				
			||||||
 | 
					  def update_invite(invite, attrs, %User{role: :admin}) do
 | 
				
			||||||
 | 
					    invite |> Invite.update_changeset(attrs) |> Repo.update()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Deletes a invite.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
				
			||||||
 | 
					      {:ok, %Invite{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
				
			||||||
 | 
					      {:error, %Changeset{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec delete_invite(Invite.t(), User.t()) ::
 | 
				
			||||||
 | 
					          {:ok, Invite.t()} | {:error, Invite.changeset()}
 | 
				
			||||||
 | 
					  def delete_invite(invite, %User{role: :admin}) do
 | 
				
			||||||
 | 
					    invite |> Repo.delete()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Deletes a invite.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
				
			||||||
 | 
					      %Invite{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec delete_invite!(Invite.t(), User.t()) :: Invite.t()
 | 
				
			||||||
 | 
					  def delete_invite!(invite, %User{role: :admin}) do
 | 
				
			||||||
 | 
					    invite |> Repo.delete!()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -6,9 +6,19 @@ defmodule Memex.Accounts.User do
 | 
				
			|||||||
  use Ecto.Schema
 | 
					  use Ecto.Schema
 | 
				
			||||||
  import Ecto.Changeset
 | 
					  import Ecto.Changeset
 | 
				
			||||||
  import MemexWeb.Gettext
 | 
					  import MemexWeb.Gettext
 | 
				
			||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Association, Changeset, UUID}
 | 
				
			||||||
  alias Memex.Invites.Invite
 | 
					  alias Memex.Accounts.{Invite, User}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :id,
 | 
				
			||||||
 | 
					             :email,
 | 
				
			||||||
 | 
					             :confirmed_at,
 | 
				
			||||||
 | 
					             :role,
 | 
				
			||||||
 | 
					             :locale,
 | 
				
			||||||
 | 
					             :inserted_at,
 | 
				
			||||||
 | 
					             :updated_at
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @derive {Inspect, except: [:password]}
 | 
					  @derive {Inspect, except: [:password]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
@@ -20,27 +30,31 @@ defmodule Memex.Accounts.User do
 | 
				
			|||||||
    field :role, Ecto.Enum, values: [:admin, :user], default: :user
 | 
					    field :role, Ecto.Enum, values: [:admin, :user], default: :user
 | 
				
			||||||
    field :locale, :string
 | 
					    field :locale, :string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    has_many :invites, Invite, on_delete: :delete_all
 | 
					    has_many :created_invites, Invite, foreign_key: :created_by_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    belongs_to :invite, Invite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    timestamps()
 | 
					    timestamps()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @type t :: %__MODULE__{
 | 
					  @type t :: %User{
 | 
				
			||||||
          id: id(),
 | 
					          id: id(),
 | 
				
			||||||
          email: String.t(),
 | 
					          email: String.t(),
 | 
				
			||||||
          password: String.t(),
 | 
					          password: String.t(),
 | 
				
			||||||
          hashed_password: String.t(),
 | 
					          hashed_password: String.t(),
 | 
				
			||||||
          confirmed_at: NaiveDateTime.t(),
 | 
					          confirmed_at: NaiveDateTime.t(),
 | 
				
			||||||
          role: role(),
 | 
					          role: role(),
 | 
				
			||||||
          invites: [Invite.t()],
 | 
					 | 
				
			||||||
          locale: String.t() | nil,
 | 
					          locale: String.t() | nil,
 | 
				
			||||||
 | 
					          created_invites: [Invite.t()] | Association.NotLoaded.t(),
 | 
				
			||||||
 | 
					          invite: Invite.t() | nil | Association.NotLoaded.t(),
 | 
				
			||||||
 | 
					          invite_id: Invite.id() | nil,
 | 
				
			||||||
          inserted_at: NaiveDateTime.t(),
 | 
					          inserted_at: NaiveDateTime.t(),
 | 
				
			||||||
          updated_at: NaiveDateTime.t()
 | 
					          updated_at: NaiveDateTime.t()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
  @type new_user :: %__MODULE__{}
 | 
					  @type new_user :: %User{}
 | 
				
			||||||
  @type id :: UUID.t()
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
  @type changeset :: Changeset.t(t() | new_user())
 | 
					  @type changeset :: Changeset.t(t() | new_user())
 | 
				
			||||||
  @type role :: :user | :admin | String.t()
 | 
					  @type role :: :admin | :user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  A user changeset for registration.
 | 
					  A user changeset for registration.
 | 
				
			||||||
@@ -59,22 +73,22 @@ defmodule Memex.Accounts.User do
 | 
				
			|||||||
      validations on a LiveView form), this option can be set to `false`.
 | 
					      validations on a LiveView form), this option can be set to `false`.
 | 
				
			||||||
      Defaults to `true`.
 | 
					      Defaults to `true`.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec registration_changeset(attrs :: map()) :: changeset()
 | 
					  @spec registration_changeset(attrs :: map(), Invite.t() | nil) :: changeset()
 | 
				
			||||||
  @spec registration_changeset(attrs :: map(), opts :: keyword()) :: changeset()
 | 
					  @spec registration_changeset(attrs :: map(), Invite.t() | nil, opts :: keyword()) :: changeset()
 | 
				
			||||||
  def registration_changeset(attrs, opts \\ []) do
 | 
					  def registration_changeset(attrs, invite, opts \\ []) do
 | 
				
			||||||
    %__MODULE__{}
 | 
					    %User{}
 | 
				
			||||||
    |> cast(attrs, [:email, :password, :locale])
 | 
					    |> cast(attrs, [:email, :password, :locale])
 | 
				
			||||||
 | 
					    |> put_change(:invite_id, if(invite, do: invite.id))
 | 
				
			||||||
    |> validate_email()
 | 
					    |> validate_email()
 | 
				
			||||||
    |> validate_password(opts)
 | 
					    |> validate_password(opts)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  A user changeset for role.
 | 
					  A user changeset for role.
 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec role_changeset(t() | new_user() | changeset(), role()) :: changeset()
 | 
					  @spec role_changeset(t() | new_user() | changeset(), role()) :: changeset()
 | 
				
			||||||
  def role_changeset(user, role) do
 | 
					  def role_changeset(user, role) do
 | 
				
			||||||
    user |> cast(%{"role" => role}, [:role])
 | 
					    user |> change(role: role)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec validate_email(changeset()) :: changeset()
 | 
					  @spec validate_email(changeset()) :: changeset()
 | 
				
			||||||
@@ -89,7 +103,8 @@ defmodule Memex.Accounts.User do
 | 
				
			|||||||
    |> unique_constraint(:email)
 | 
					    |> unique_constraint(:email)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @spec validate_password(changeset(), opts :: keyword()) :: changeset()
 | 
					  @spec validate_password(changeset(), opts :: keyword()) ::
 | 
				
			||||||
 | 
					          changeset()
 | 
				
			||||||
  defp validate_password(changeset, opts) do
 | 
					  defp validate_password(changeset, opts) do
 | 
				
			||||||
    changeset
 | 
					    changeset
 | 
				
			||||||
    |> validate_required([:password])
 | 
					    |> validate_required([:password])
 | 
				
			||||||
@@ -167,12 +182,12 @@ defmodule Memex.Accounts.User do
 | 
				
			|||||||
  `Bcrypt.no_user_verify/0` to avoid timing attacks.
 | 
					  `Bcrypt.no_user_verify/0` to avoid timing attacks.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  @spec valid_password?(t(), String.t()) :: boolean()
 | 
					  @spec valid_password?(t(), String.t()) :: boolean()
 | 
				
			||||||
  def valid_password?(%__MODULE__{hashed_password: hashed_password}, password)
 | 
					  def valid_password?(%User{hashed_password: hashed_password}, password)
 | 
				
			||||||
      when is_binary(hashed_password) and byte_size(password) > 0 do
 | 
					      when is_binary(hashed_password) and byte_size(password) > 0 do
 | 
				
			||||||
    Bcrypt.verify_pass(password, hashed_password)
 | 
					    Bcrypt.verify_pass(password, hashed_password)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def valid_password?(_, _) do
 | 
					  def valid_password?(_invalid_user, _invalid_password) do
 | 
				
			||||||
    Bcrypt.no_user_verify()
 | 
					    Bcrypt.no_user_verify()
 | 
				
			||||||
    false
 | 
					    false
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,77 +0,0 @@
 | 
				
			|||||||
defmodule Memex.Accounts.UserNotifier do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Contains templates and messages for user messages
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # 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
 | 
					 | 
				
			||||||
@@ -5,6 +5,8 @@ defmodule Memex.Accounts.UserToken do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  use Ecto.Schema
 | 
					  use Ecto.Schema
 | 
				
			||||||
  import Ecto.Query
 | 
					  import Ecto.Query
 | 
				
			||||||
 | 
					  alias Ecto.{Association, UUID}
 | 
				
			||||||
 | 
					  alias Memex.Accounts.User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @hash_algorithm :sha256
 | 
					  @hash_algorithm :sha256
 | 
				
			||||||
  @rand_size 32
 | 
					  @rand_size 32
 | 
				
			||||||
@@ -22,11 +24,25 @@ defmodule Memex.Accounts.UserToken do
 | 
				
			|||||||
    field :token, :binary
 | 
					    field :token, :binary
 | 
				
			||||||
    field :context, :string
 | 
					    field :context, :string
 | 
				
			||||||
    field :sent_to, :string
 | 
					    field :sent_to, :string
 | 
				
			||||||
    belongs_to :user, Memex.Accounts.User
 | 
					
 | 
				
			||||||
 | 
					    belongs_to :user, User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    timestamps(updated_at: false)
 | 
					    timestamps(updated_at: false)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @type t :: %__MODULE__{
 | 
				
			||||||
 | 
					          id: id(),
 | 
				
			||||||
 | 
					          token: token(),
 | 
				
			||||||
 | 
					          context: String.t(),
 | 
				
			||||||
 | 
					          sent_to: String.t(),
 | 
				
			||||||
 | 
					          user: User.t() | Association.NotLoaded.t(),
 | 
				
			||||||
 | 
					          user_id: User.id() | nil,
 | 
				
			||||||
 | 
					          inserted_at: NaiveDateTime.t()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					  @type new_user_token :: %__MODULE__{}
 | 
				
			||||||
 | 
					  @type id :: UUID.t()
 | 
				
			||||||
 | 
					  @type token :: binary()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Generates a token that will be stored in a signed place,
 | 
					  Generates a token that will be stored in a signed place,
 | 
				
			||||||
  such as session or cookie. As they are signed, those
 | 
					  such as session or cookie. As they are signed, those
 | 
				
			||||||
@@ -34,7 +50,7 @@ defmodule Memex.Accounts.UserToken do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  def build_session_token(user) do
 | 
					  def build_session_token(user) do
 | 
				
			||||||
    token = :crypto.strong_rand_bytes(@rand_size)
 | 
					    token = :crypto.strong_rand_bytes(@rand_size)
 | 
				
			||||||
    {token, %Memex.Accounts.UserToken{token: token, context: "session", user_id: user.id}}
 | 
					    {token, %__MODULE__{token: token, context: "session", user_id: user.id}}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -69,7 +85,7 @@ defmodule Memex.Accounts.UserToken do
 | 
				
			|||||||
    hashed_token = :crypto.hash(@hash_algorithm, token)
 | 
					    hashed_token = :crypto.hash(@hash_algorithm, token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {Base.url_encode64(token, padding: false),
 | 
					    {Base.url_encode64(token, padding: false),
 | 
				
			||||||
     %Memex.Accounts.UserToken{
 | 
					     %__MODULE__{
 | 
				
			||||||
       token: hashed_token,
 | 
					       token: hashed_token,
 | 
				
			||||||
       context: context,
 | 
					       context: context,
 | 
				
			||||||
       sent_to: sent_to,
 | 
					       sent_to: sent_to,
 | 
				
			||||||
@@ -129,17 +145,17 @@ defmodule Memex.Accounts.UserToken do
 | 
				
			|||||||
  Returns the given token with the given context.
 | 
					  Returns the given token with the given context.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  def token_and_context_query(token, context) do
 | 
					  def token_and_context_query(token, context) do
 | 
				
			||||||
    from Memex.Accounts.UserToken, where: [token: ^token, context: ^context]
 | 
					    from __MODULE__, where: [token: ^token, context: ^context]
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Gets all tokens for the given user for the given contexts.
 | 
					  Gets all tokens for the given user for the given contexts.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  def user_and_contexts_query(user, :all) do
 | 
					  def user_and_contexts_query(user, :all) do
 | 
				
			||||||
    from t in Memex.Accounts.UserToken, where: t.user_id == ^user.id
 | 
					    from t in __MODULE__, where: t.user_id == ^user.id
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def user_and_contexts_query(user, [_ | _] = contexts) do
 | 
					  def user_and_contexts_query(user, [_ | _] = contexts) do
 | 
				
			||||||
    from t in Memex.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts
 | 
					    from t in __MODULE__, where: t.user_id == ^user.id and t.context in ^contexts
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ defmodule Memex.Application do
 | 
				
			|||||||
  @moduledoc false
 | 
					  @moduledoc false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  use Application
 | 
					  use Application
 | 
				
			||||||
 | 
					  alias Memex.Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @impl true
 | 
					  @impl true
 | 
				
			||||||
  def start(_type, _args) do
 | 
					  def start(_type, _args) do
 | 
				
			||||||
@@ -17,16 +18,24 @@ defmodule Memex.Application do
 | 
				
			|||||||
      # Start the Endpoint (http/https)
 | 
					      # Start the Endpoint (http/https)
 | 
				
			||||||
      MemexWeb.Endpoint,
 | 
					      MemexWeb.Endpoint,
 | 
				
			||||||
      # Add Oban
 | 
					      # Add Oban
 | 
				
			||||||
      {Oban, oban_config()}
 | 
					      {Oban, oban_config()},
 | 
				
			||||||
 | 
					      Memex.Repo.Migrator
 | 
				
			||||||
      # Start a worker by calling: Memex.Worker.start_link(arg)
 | 
					      # Start a worker by calling: Memex.Worker.start_link(arg)
 | 
				
			||||||
      # {Memex.Worker, arg}
 | 
					      # {Memex.Worker, arg}
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Automatically migrate on start in prod
 | 
					    # Oban events logging https://hexdocs.pm/oban/Oban.html#module-reporting-errors
 | 
				
			||||||
    children =
 | 
					    :ok =
 | 
				
			||||||
      if Application.get_env(:memex, Memex.Application, automigrate: false)[:automigrate],
 | 
					      :telemetry.attach_many(
 | 
				
			||||||
        do: children ++ [Memex.Repo.Migrator],
 | 
					        "oban-logger",
 | 
				
			||||||
        else: children
 | 
					        [
 | 
				
			||||||
 | 
					          [:oban, :job, :exception],
 | 
				
			||||||
 | 
					          [:oban, :job, :start],
 | 
				
			||||||
 | 
					          [:oban, :job, :stop]
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        &Logger.handle_event/4,
 | 
				
			||||||
 | 
					        []
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # See https://hexdocs.pm/elixir/Supervisor.html
 | 
					    # See https://hexdocs.pm/elixir/Supervisor.html
 | 
				
			||||||
    # for other strategies and supported options
 | 
					    # for other strategies and supported options
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -228,4 +228,13 @@ defmodule Memex.Contexts do
 | 
				
			|||||||
  def change_context(%Context{} = context, attrs \\ %{}, user) do
 | 
					  def change_context(%Context{} = context, attrs \\ %{}, user) do
 | 
				
			||||||
    context |> Context.update_changeset(attrs, user)
 | 
					    context |> Context.update_changeset(attrs, user)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec is_owner_or_admin?(Context.t(), User.t()) :: boolean()
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(_context, %{role: :admin}), do: true
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(_context, _other_user), do: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec is_owner?(Context.t(), User.t()) :: boolean()
 | 
				
			||||||
 | 
					  def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
				
			||||||
 | 
					  def is_owner?(_context, _other_user), do: false
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,16 @@ defmodule Memex.Contexts.Context do
 | 
				
			|||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
  alias Memex.{Accounts.User, Repo}
 | 
					  alias Memex.{Accounts.User, Repo}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Phoenix.Param, key: :slug}
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :slug,
 | 
				
			||||||
 | 
					             :content,
 | 
				
			||||||
 | 
					             :tags,
 | 
				
			||||||
 | 
					             :visibility,
 | 
				
			||||||
 | 
					             :inserted_at,
 | 
				
			||||||
 | 
					             :updated_at
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "contexts" do
 | 
					  schema "contexts" do
 | 
				
			||||||
@@ -18,7 +28,7 @@ defmodule Memex.Contexts.Context do
 | 
				
			|||||||
    field :tags_string, :string, virtual: true
 | 
					    field :tags_string, :string, virtual: true
 | 
				
			||||||
    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
					    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    belongs_to :user, User
 | 
					    field :user_id, :binary_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    timestamps()
 | 
					    timestamps()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -29,7 +39,6 @@ defmodule Memex.Contexts.Context do
 | 
				
			|||||||
          tags: [String.t()] | nil,
 | 
					          tags: [String.t()] | nil,
 | 
				
			||||||
          tags_string: String.t() | nil,
 | 
					          tags_string: String.t() | nil,
 | 
				
			||||||
          visibility: :public | :private | :unlisted,
 | 
					          visibility: :public | :private | :unlisted,
 | 
				
			||||||
          user: User.t() | Ecto.Association.NotLoaded.t(),
 | 
					 | 
				
			||||||
          user_id: User.id(),
 | 
					          user_id: User.id(),
 | 
				
			||||||
          inserted_at: NaiveDateTime.t(),
 | 
					          inserted_at: NaiveDateTime.t(),
 | 
				
			||||||
          updated_at: NaiveDateTime.t()
 | 
					          updated_at: NaiveDateTime.t()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,173 +0,0 @@
 | 
				
			|||||||
defmodule Memex.Invites do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  The Invites context.
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  import Ecto.Query, warn: false
 | 
					 | 
				
			||||||
  alias Ecto.Changeset
 | 
					 | 
				
			||||||
  alias Memex.{Accounts.User, Invites.Invite, Repo}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @invite_token_length 20
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns the list of invites.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> list_invites(%User{id: 123, role: :admin})
 | 
					 | 
				
			||||||
      [%Invite{}, ...]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec list_invites(User.t()) :: [Invite.t()]
 | 
					 | 
				
			||||||
  def list_invites(%User{role: :admin}) do
 | 
					 | 
				
			||||||
    Repo.all(from i in Invite, order_by: i.name)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Gets a single invite.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Raises `Ecto.NoResultsError` if the Invite does not exist.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> get_invite!(123, %User{id: 123, role: :admin})
 | 
					 | 
				
			||||||
      %Invite{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> get_invite!(456, %User{id: 123, role: :admin})
 | 
					 | 
				
			||||||
      ** (Ecto.NoResultsError)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec get_invite!(Invite.id(), User.t()) :: Invite.t()
 | 
					 | 
				
			||||||
  def get_invite!(id, %User{role: :admin}) do
 | 
					 | 
				
			||||||
    Repo.get!(Invite, id)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns a valid invite or nil based on the attempted token
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> get_invite_by_token("valid_token")
 | 
					 | 
				
			||||||
      %Invite{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> get_invite_by_token("invalid_token")
 | 
					 | 
				
			||||||
      nil
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec get_invite_by_token(token :: String.t() | nil) :: Invite.t() | nil
 | 
					 | 
				
			||||||
  def get_invite_by_token(nil), do: nil
 | 
					 | 
				
			||||||
  def get_invite_by_token(""), do: nil
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def get_invite_by_token(token) do
 | 
					 | 
				
			||||||
    Repo.one(
 | 
					 | 
				
			||||||
      from(i in Invite,
 | 
					 | 
				
			||||||
        where: i.token == ^token and i.disabled_at |> is_nil()
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Uses invite by decrementing uses_left, or marks invite invalid if it's been
 | 
					 | 
				
			||||||
  completely used.
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec use_invite!(Invite.t()) :: Invite.t()
 | 
					 | 
				
			||||||
  def use_invite!(%Invite{uses_left: nil} = invite), do: invite
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def use_invite!(%Invite{uses_left: uses_left} = invite) do
 | 
					 | 
				
			||||||
    new_uses_left = uses_left - 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    attrs =
 | 
					 | 
				
			||||||
      if new_uses_left <= 0 do
 | 
					 | 
				
			||||||
        now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
 | 
					 | 
				
			||||||
        %{"uses_left" => 0, "disabled_at" => now}
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        %{"uses_left" => new_uses_left}
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    invite |> Invite.update_changeset(attrs) |> Repo.update!()
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Creates a invite.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> create_invite(%User{id: 123, role: :admin}, %{field: value})
 | 
					 | 
				
			||||||
      {:ok, %Invite{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> create_invite(%User{id: 123, role: :admin}, %{field: bad_value})
 | 
					 | 
				
			||||||
      {:error, %Changeset{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec create_invite(User.t(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.new_invite())}
 | 
					 | 
				
			||||||
  def create_invite(%User{id: user_id, role: :admin}, attrs) do
 | 
					 | 
				
			||||||
    token =
 | 
					 | 
				
			||||||
      :crypto.strong_rand_bytes(@invite_token_length)
 | 
					 | 
				
			||||||
      |> Base.url_encode64()
 | 
					 | 
				
			||||||
      |> binary_part(0, @invite_token_length)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    attrs = attrs |> Map.merge(%{"user_id" => user_id, "token" => token})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    %Invite{} |> Invite.create_changeset(attrs) |> Repo.insert()
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Updates a invite.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> update_invite(invite, %{field: new_value}, %User{id: 123, role: :admin})
 | 
					 | 
				
			||||||
      {:ok, %Invite{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> update_invite(invite, %{field: bad_value}, %User{id: 123, role: :admin})
 | 
					 | 
				
			||||||
      {:error, %Changeset{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec update_invite(Invite.t(), attrs :: map(), User.t()) ::
 | 
					 | 
				
			||||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.t())}
 | 
					 | 
				
			||||||
  def update_invite(invite, attrs, %User{role: :admin}),
 | 
					 | 
				
			||||||
    do: invite |> Invite.update_changeset(attrs) |> Repo.update()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Deletes a invite.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
					 | 
				
			||||||
      {:ok, %Invite{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
					 | 
				
			||||||
      {:error, %Changeset{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec delete_invite(Invite.t(), User.t()) ::
 | 
					 | 
				
			||||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.t())}
 | 
					 | 
				
			||||||
  def delete_invite(invite, %User{role: :admin}), do: invite |> Repo.delete()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Deletes a invite.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
					 | 
				
			||||||
      %Invite{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec delete_invite!(Invite.t(), User.t()) :: Invite.t()
 | 
					 | 
				
			||||||
  def delete_invite!(invite, %User{role: :admin}), do: invite |> Repo.delete!()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @doc """
 | 
					 | 
				
			||||||
  Returns an `%Changeset{}` for tracking invite changes.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ## Examples
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iex> change_invite(invite)
 | 
					 | 
				
			||||||
      %Changeset{data: %Invite{}}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  @spec change_invite(Invite.t() | Invite.new_invite()) ::
 | 
					 | 
				
			||||||
          Changeset.t(Invite.t() | Invite.new_invite())
 | 
					 | 
				
			||||||
  @spec change_invite(Invite.t() | Invite.new_invite(), attrs :: map()) ::
 | 
					 | 
				
			||||||
          Changeset.t(Invite.t() | Invite.new_invite())
 | 
					 | 
				
			||||||
  def change_invite(invite, attrs \\ %{}), do: invite |> Invite.update_changeset(attrs)
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
							
								
								
									
										63
									
								
								lib/memex/logger.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/memex/logger.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					defmodule Memex.Logger do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  Custom logger for telemetry events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Oban implementation taken from
 | 
				
			||||||
 | 
					  https://hexdocs.pm/oban/Oban.html#module-reporting-errors
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  require Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event([:oban, :job, :exception], measure, %{stacktrace: stacktrace} = meta, _config) do
 | 
				
			||||||
 | 
					    data =
 | 
				
			||||||
 | 
					      get_oban_job_data(meta, measure)
 | 
				
			||||||
 | 
					      |> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace))
 | 
				
			||||||
 | 
					      |> pretty_encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Logger.error("#{meta.reason} #{data}")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event([:oban, :job, :start], measure, meta, _config) do
 | 
				
			||||||
 | 
					    data = get_oban_job_data(meta, measure) |> pretty_encode()
 | 
				
			||||||
 | 
					    Logger.info("Started oban job: #{data}")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event([:oban, :job, :stop], measure, meta, _config) do
 | 
				
			||||||
 | 
					    data = get_oban_job_data(meta, measure) |> pretty_encode()
 | 
				
			||||||
 | 
					    Logger.info("Finished oban job: #{data}")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event([:oban, :job, unhandled_event], measure, meta, _config) do
 | 
				
			||||||
 | 
					    data =
 | 
				
			||||||
 | 
					      get_oban_job_data(meta, measure)
 | 
				
			||||||
 | 
					      |> Map.put(:event, unhandled_event)
 | 
				
			||||||
 | 
					      |> pretty_encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Logger.warning("Unhandled oban job event: #{data}")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_event(unhandled_event, measure, meta, config) do
 | 
				
			||||||
 | 
					    data =
 | 
				
			||||||
 | 
					      pretty_encode(%{
 | 
				
			||||||
 | 
					        event: unhandled_event,
 | 
				
			||||||
 | 
					        meta: meta,
 | 
				
			||||||
 | 
					        measurements: measure,
 | 
				
			||||||
 | 
					        config: config
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Logger.warning("Unhandled telemetry event: #{data}")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp get_oban_job_data(%{job: job}, measure) do
 | 
				
			||||||
 | 
					    %{
 | 
				
			||||||
 | 
					      job: job |> Map.take([:id, :args, :meta, :queue, :worker]),
 | 
				
			||||||
 | 
					      measurements: measure
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp pretty_encode(data) do
 | 
				
			||||||
 | 
					    data
 | 
				
			||||||
 | 
					    |> Jason.encode!()
 | 
				
			||||||
 | 
					    |> Jason.Formatter.pretty_print()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -228,4 +228,13 @@ defmodule Memex.Notes do
 | 
				
			|||||||
  def change_note(%Note{} = note, attrs \\ %{}, user) do
 | 
					  def change_note(%Note{} = note, attrs \\ %{}, user) do
 | 
				
			||||||
    note |> Note.update_changeset(attrs, user)
 | 
					    note |> Note.update_changeset(attrs, user)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec is_owner_or_admin?(Note.t(), User.t()) :: boolean()
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(_context, %{role: :admin}), do: true
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(_context, _other_user), do: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec is_owner?(Note.t(), User.t()) :: boolean()
 | 
				
			||||||
 | 
					  def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
				
			||||||
 | 
					  def is_owner?(_context, _other_user), do: false
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,16 @@ defmodule Memex.Notes.Note do
 | 
				
			|||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
  alias Memex.{Accounts.User, Repo}
 | 
					  alias Memex.{Accounts.User, Repo}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Phoenix.Param, key: :slug}
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :slug,
 | 
				
			||||||
 | 
					             :content,
 | 
				
			||||||
 | 
					             :tags,
 | 
				
			||||||
 | 
					             :visibility,
 | 
				
			||||||
 | 
					             :inserted_at,
 | 
				
			||||||
 | 
					             :updated_at
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "notes" do
 | 
					  schema "notes" do
 | 
				
			||||||
@@ -17,7 +27,7 @@ defmodule Memex.Notes.Note do
 | 
				
			|||||||
    field :tags_string, :string, virtual: true
 | 
					    field :tags_string, :string, virtual: true
 | 
				
			||||||
    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
					    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    belongs_to :user, User
 | 
					    field :user_id, :binary_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    timestamps()
 | 
					    timestamps()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -28,7 +38,6 @@ defmodule Memex.Notes.Note do
 | 
				
			|||||||
          tags: [String.t()] | nil,
 | 
					          tags: [String.t()] | nil,
 | 
				
			||||||
          tags_string: String.t() | nil,
 | 
					          tags_string: String.t() | nil,
 | 
				
			||||||
          visibility: :public | :private | :unlisted,
 | 
					          visibility: :public | :private | :unlisted,
 | 
				
			||||||
          user: User.t() | Ecto.Association.NotLoaded.t(),
 | 
					 | 
				
			||||||
          user_id: User.id(),
 | 
					          user_id: User.id(),
 | 
				
			||||||
          inserted_at: NaiveDateTime.t(),
 | 
					          inserted_at: NaiveDateTime.t(),
 | 
				
			||||||
          updated_at: NaiveDateTime.t()
 | 
					          updated_at: NaiveDateTime.t()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -230,4 +230,13 @@ defmodule Memex.Pipelines do
 | 
				
			|||||||
  def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}, user) do
 | 
					  def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}, user) do
 | 
				
			||||||
    pipeline |> Pipeline.update_changeset(attrs, user)
 | 
					    pipeline |> Pipeline.update_changeset(attrs, user)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec is_owner_or_admin?(Pipeline.t(), User.t()) :: boolean()
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(_context, %{role: :admin}), do: true
 | 
				
			||||||
 | 
					  def is_owner_or_admin?(_context, _other_user), do: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec is_owner?(Pipeline.t(), User.t()) :: boolean()
 | 
				
			||||||
 | 
					  def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
 | 
				
			||||||
 | 
					  def is_owner?(_context, _other_user), do: false
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,17 @@ defmodule Memex.Pipelines.Pipeline do
 | 
				
			|||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
  alias Memex.{Accounts.User, Pipelines.Steps.Step, Repo}
 | 
					  alias Memex.{Accounts.User, Pipelines.Steps.Step, Repo}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Phoenix.Param, key: :slug}
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :slug,
 | 
				
			||||||
 | 
					             :description,
 | 
				
			||||||
 | 
					             :tags,
 | 
				
			||||||
 | 
					             :visibility,
 | 
				
			||||||
 | 
					             :inserted_at,
 | 
				
			||||||
 | 
					             :steps,
 | 
				
			||||||
 | 
					             :updated_at
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "pipelines" do
 | 
					  schema "pipelines" do
 | 
				
			||||||
@@ -17,7 +28,7 @@ defmodule Memex.Pipelines.Pipeline do
 | 
				
			|||||||
    field :tags_string, :string, virtual: true
 | 
					    field :tags_string, :string, virtual: true
 | 
				
			||||||
    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
					    field :visibility, Ecto.Enum, values: [:public, :private, :unlisted]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    belongs_to :user, User
 | 
					    field :user_id, :binary_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    has_many :steps, Step, preload_order: [asc: :position]
 | 
					    has_many :steps, Step, preload_order: [asc: :position]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,7 +41,6 @@ defmodule Memex.Pipelines.Pipeline do
 | 
				
			|||||||
          tags: [String.t()] | nil,
 | 
					          tags: [String.t()] | nil,
 | 
				
			||||||
          tags_string: String.t() | nil,
 | 
					          tags_string: String.t() | nil,
 | 
				
			||||||
          visibility: :public | :private | :unlisted,
 | 
					          visibility: :public | :private | :unlisted,
 | 
				
			||||||
          user: User.t() | Ecto.Association.NotLoaded.t(),
 | 
					 | 
				
			||||||
          user_id: User.id(),
 | 
					          user_id: User.id(),
 | 
				
			||||||
          inserted_at: NaiveDateTime.t(),
 | 
					          inserted_at: NaiveDateTime.t(),
 | 
				
			||||||
          updated_at: NaiveDateTime.t()
 | 
					          updated_at: NaiveDateTime.t()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,14 @@ defmodule Memex.Pipelines.Steps.Step do
 | 
				
			|||||||
  alias Ecto.{Changeset, UUID}
 | 
					  alias Ecto.{Changeset, UUID}
 | 
				
			||||||
  alias Memex.{Accounts.User, Pipelines.Pipeline}
 | 
					  alias Memex.{Accounts.User, Pipelines.Pipeline}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @derive {Jason.Encoder,
 | 
				
			||||||
 | 
					           only: [
 | 
				
			||||||
 | 
					             :title,
 | 
				
			||||||
 | 
					             :content,
 | 
				
			||||||
 | 
					             :position,
 | 
				
			||||||
 | 
					             :inserted_at,
 | 
				
			||||||
 | 
					             :updated_at
 | 
				
			||||||
 | 
					           ]}
 | 
				
			||||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
					  @primary_key {:id, :binary_id, autogenerate: true}
 | 
				
			||||||
  @foreign_key_type :binary_id
 | 
					  @foreign_key_type :binary_id
 | 
				
			||||||
  schema "steps" do
 | 
					  schema "steps" do
 | 
				
			||||||
@@ -15,7 +23,7 @@ defmodule Memex.Pipelines.Steps.Step do
 | 
				
			|||||||
    field :position, :integer
 | 
					    field :position, :integer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    belongs_to :pipeline, Pipeline
 | 
					    belongs_to :pipeline, Pipeline
 | 
				
			||||||
    belongs_to :user, User
 | 
					    field :user_id, :binary_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    timestamps()
 | 
					    timestamps()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -26,7 +34,6 @@ defmodule Memex.Pipelines.Steps.Step do
 | 
				
			|||||||
          position: non_neg_integer(),
 | 
					          position: non_neg_integer(),
 | 
				
			||||||
          pipeline: Pipeline.t() | Ecto.Association.NotLoaded.t(),
 | 
					          pipeline: Pipeline.t() | Ecto.Association.NotLoaded.t(),
 | 
				
			||||||
          pipeline_id: Pipeline.id(),
 | 
					          pipeline_id: Pipeline.id(),
 | 
				
			||||||
          user: User.t() | Ecto.Association.NotLoaded.t(),
 | 
					 | 
				
			||||||
          user_id: User.id(),
 | 
					          user_id: User.id(),
 | 
				
			||||||
          inserted_at: NaiveDateTime.t(),
 | 
					          inserted_at: NaiveDateTime.t(),
 | 
				
			||||||
          updated_at: NaiveDateTime.t()
 | 
					          updated_at: NaiveDateTime.t()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,9 @@ defmodule Memex.Release do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def rollback(repo, version) do
 | 
					  def rollback(repo, version) do
 | 
				
			||||||
    load_app()
 | 
					    load_app()
 | 
				
			||||||
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
 | 
					
 | 
				
			||||||
 | 
					    {:ok, _fun_return, _apps} =
 | 
				
			||||||
 | 
					      Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp load_app do
 | 
					  defp load_app do
 | 
				
			||||||
@@ -18,7 +20,8 @@ defmodule Memex.Release do
 | 
				
			|||||||
    load_app()
 | 
					    load_app()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for repo <- Application.fetch_env!(@app, :ecto_repos) do
 | 
					    for repo <- Application.fetch_env!(@app, :ecto_repos) do
 | 
				
			||||||
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
 | 
					      {:ok, _fun_return, _apps} =
 | 
				
			||||||
 | 
					        Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,17 +6,20 @@ defmodule Memex.Repo.Migrator do
 | 
				
			|||||||
  use GenServer
 | 
					  use GenServer
 | 
				
			||||||
  require Logger
 | 
					  require Logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def start_link(_) do
 | 
					  def start_link(_opts) do
 | 
				
			||||||
    GenServer.start_link(__MODULE__, [], [])
 | 
					    GenServer.start_link(__MODULE__, [], [])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def init(_) do
 | 
					  def init(_opts) do
 | 
				
			||||||
    migrate!()
 | 
					    {:ok, if(automigrate_enabled?(), do: migrate!())}
 | 
				
			||||||
    {:ok, nil}
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def migrate! do
 | 
					  def migrate! do
 | 
				
			||||||
    path = Application.app_dir(:memex, "priv/repo/migrations")
 | 
					    path = Application.app_dir(:memex, "priv/repo/migrations")
 | 
				
			||||||
    Ecto.Migrator.run(Memex.Repo, path, :up, all: true)
 | 
					    Ecto.Migrator.run(Memex.Repo, path, :up, all: true)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp automigrate_enabled? do
 | 
				
			||||||
 | 
					    Application.get_env(:memex, Memex.Application, automigrate: false)[:automigrate]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										138
									
								
								lib/memex_web.ex
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								lib/memex_web.ex
									
									
									
									
									
								
							@@ -1,78 +1,29 @@
 | 
				
			|||||||
defmodule MemexWeb do
 | 
					defmodule MemexWeb do
 | 
				
			||||||
  @moduledoc """
 | 
					  @moduledoc """
 | 
				
			||||||
  The entrypoint for defining your web interface, such
 | 
					  The entrypoint for defining your web interface, such
 | 
				
			||||||
  as controllers, views, channels and so on.
 | 
					  as controllers, components, channels, and so on.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  This can be used in your application as:
 | 
					  This can be used in your application as:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      use MemexWeb, :controller
 | 
					      use MemexWeb, :controller
 | 
				
			||||||
      use MemexWeb, :view
 | 
					      use MemexWeb, :html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  The definitions below will be executed for every view,
 | 
					  The definitions below will be executed for every controller,
 | 
				
			||||||
  controller, etc, so keep them short and clean, focused
 | 
					  component, etc, so keep them short and clean, focused
 | 
				
			||||||
  on imports, uses and aliases.
 | 
					  on imports, uses and aliases.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Do NOT define functions inside the quoted expressions
 | 
					  Do NOT define functions inside the quoted expressions
 | 
				
			||||||
  below. Instead, define any helper function in modules
 | 
					  below. Instead, define additional modules and import
 | 
				
			||||||
  and import those modules here.
 | 
					  those modules here.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def controller do
 | 
					  def static_paths, do: ~w(css js fonts images favicon.ico robots.txt)
 | 
				
			||||||
    quote do
 | 
					 | 
				
			||||||
      use Phoenix.Controller, namespace: MemexWeb
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      import Plug.Conn
 | 
					 | 
				
			||||||
      import MemexWeb.Gettext
 | 
					 | 
				
			||||||
      alias MemexWeb.Endpoint
 | 
					 | 
				
			||||||
      alias MemexWeb.Router.Helpers, as: Routes
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def view do
 | 
					 | 
				
			||||||
    quote do
 | 
					 | 
				
			||||||
      use Phoenix.View,
 | 
					 | 
				
			||||||
        root: "lib/memex_web/templates",
 | 
					 | 
				
			||||||
        namespace: MemexWeb
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      # 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: {MemexWeb.LayoutView, :live}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      on_mount MemexWeb.InitAssigns
 | 
					 | 
				
			||||||
      unquote(view_helpers())
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def live_component do
 | 
					 | 
				
			||||||
    quote do
 | 
					 | 
				
			||||||
      use Phoenix.LiveComponent
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      unquote(view_helpers())
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def component do
 | 
					 | 
				
			||||||
    quote do
 | 
					 | 
				
			||||||
      use Phoenix.Component
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      unquote(view_helpers())
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def router do
 | 
					  def router do
 | 
				
			||||||
    quote do
 | 
					    quote do
 | 
				
			||||||
      use Phoenix.Router
 | 
					      use Phoenix.Router, helpers: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # Import common connection and controller functions to use in pipelines
 | 
				
			||||||
      import Plug.Conn
 | 
					      import Plug.Conn
 | 
				
			||||||
      import Phoenix.Controller
 | 
					      import Phoenix.Controller
 | 
				
			||||||
      import Phoenix.LiveView.Router
 | 
					      import Phoenix.LiveView.Router
 | 
				
			||||||
@@ -82,25 +33,76 @@ defmodule MemexWeb do
 | 
				
			|||||||
  def channel do
 | 
					  def channel do
 | 
				
			||||||
    quote do
 | 
					    quote do
 | 
				
			||||||
      use Phoenix.Channel
 | 
					      use Phoenix.Channel
 | 
				
			||||||
      import MemexWeb.Gettext
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp view_helpers do
 | 
					  def controller do
 | 
				
			||||||
    quote do
 | 
					    quote do
 | 
				
			||||||
      # Use all HTML functionality (forms, tags, etc)
 | 
					      use Phoenix.Controller,
 | 
				
			||||||
 | 
					        formats: [:html, :json],
 | 
				
			||||||
 | 
					        layouts: [html: MemexWeb.Layouts]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
 | 
					      import Plug.Conn
 | 
				
			||||||
 | 
					      import MemexWeb.Gettext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      unquote(verified_routes())
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def live_view do
 | 
				
			||||||
 | 
					    quote do
 | 
				
			||||||
 | 
					      use Phoenix.LiveView,
 | 
				
			||||||
 | 
					        layout: {MemexWeb.Layouts, :app}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      unquote(html_helpers())
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def live_component do
 | 
				
			||||||
 | 
					    quote do
 | 
				
			||||||
 | 
					      use Phoenix.LiveComponent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      unquote(html_helpers())
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def html do
 | 
				
			||||||
 | 
					    quote do
 | 
				
			||||||
 | 
					      use Phoenix.Component
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # Import convenience functions from controllers
 | 
				
			||||||
 | 
					      import Phoenix.Controller,
 | 
				
			||||||
 | 
					        only: [get_csrf_token: 0, view_module: 1, view_template: 1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # Include general helpers for rendering HTML
 | 
				
			||||||
 | 
					      unquote(html_helpers())
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp html_helpers do
 | 
				
			||||||
 | 
					    quote do
 | 
				
			||||||
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
      use Phoenix.HTML
 | 
					      use Phoenix.HTML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
 | 
					      # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
 | 
				
			||||||
      import Phoenix.Component
 | 
					      import Phoenix.Component
 | 
				
			||||||
      import MemexWeb.LiveHelpers
 | 
					      import MemexWeb.{ErrorHelpers, Gettext, CoreComponents, HTMLHelpers}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # Import basic rendering functionality (render, render_layout, etc)
 | 
					      # Shortcut for generating JS commands
 | 
				
			||||||
      import Phoenix.View
 | 
					      alias Phoenix.LiveView.JS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      import MemexWeb.{ErrorHelpers, Gettext, LiveHelpers, ViewHelpers}
 | 
					      # Routes generation with the ~p sigil
 | 
				
			||||||
      alias MemexWeb.Endpoint
 | 
					      unquote(verified_routes())
 | 
				
			||||||
      alias MemexWeb.Router.Helpers, as: Routes
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def verified_routes do
 | 
				
			||||||
 | 
					    quote do
 | 
				
			||||||
 | 
					      use Phoenix.VerifiedRoutes,
 | 
				
			||||||
 | 
					        endpoint: MemexWeb.Endpoint,
 | 
				
			||||||
 | 
					        router: MemexWeb.Router,
 | 
				
			||||||
 | 
					        statics: MemexWeb.static_paths()
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,45 +0,0 @@
 | 
				
			|||||||
defmodule MemexWeb.Components.ContextContent do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Display the content for a context
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  use MemexWeb, :component
 | 
					 | 
				
			||||||
  alias Memex.Contexts.Context
 | 
					 | 
				
			||||||
  alias Phoenix.HTML
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  attr :context, Context, required: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def context_content(assigns) do
 | 
					 | 
				
			||||||
    ~H"""
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      id={"show-context-content-#{@context.id}"}
 | 
					 | 
				
			||||||
      class="input input-primary h-128 min-h-128 inline-block overflow-x-hidden overflow-y-auto"
 | 
					 | 
				
			||||||
      phx-hook="MaintainAttrs"
 | 
					 | 
				
			||||||
      phx-update="ignore"
 | 
					 | 
				
			||||||
      readonly
 | 
					 | 
				
			||||||
      phx-no-format
 | 
					 | 
				
			||||||
    ><p class="inline"><%= add_links_to_content(@context.content) %></p></div>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp add_links_to_content(content) do
 | 
					 | 
				
			||||||
    Regex.replace(
 | 
					 | 
				
			||||||
      ~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
 | 
					 | 
				
			||||||
      content,
 | 
					 | 
				
			||||||
      fn _whole_match, slug ->
 | 
					 | 
				
			||||||
        link =
 | 
					 | 
				
			||||||
          HTML.Link.link(
 | 
					 | 
				
			||||||
            "[[#{slug}]]",
 | 
					 | 
				
			||||||
            to: Routes.note_show_path(Endpoint, :show, slug),
 | 
					 | 
				
			||||||
            class: "link inline",
 | 
					 | 
				
			||||||
            data: [qa: "context-note-#{slug}"]
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
          |> HTML.Safe.to_iodata()
 | 
					 | 
				
			||||||
          |> IO.iodata_to_binary()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        "</p>#{link}<p class=\"inline\">"
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    |> String.replace("\n", "<br>")
 | 
					 | 
				
			||||||
    |> HTML.raw()
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@@ -40,7 +40,7 @@ defmodule MemexWeb.Components.ContextsTableComponent do
 | 
				
			|||||||
      if actions == [] or current_user |> is_nil() do
 | 
					      if actions == [] or current_user |> is_nil() do
 | 
				
			||||||
        []
 | 
					        []
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        [%{label: nil, key: :actions, sortable: false}]
 | 
					        [%{label: gettext("actions"), key: :actions, sortable: false}]
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    columns = [
 | 
					    columns = [
 | 
				
			||||||
@@ -88,15 +88,9 @@ defmodule MemexWeb.Components.ContextsTableComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @spec get_value_for_key(atom(), Context.t(), additional_data :: map()) ::
 | 
					  @spec get_value_for_key(atom(), Context.t(), additional_data :: map()) ::
 | 
				
			||||||
          any() | {any(), Rendered.t()}
 | 
					          any() | {any(), Rendered.t()}
 | 
				
			||||||
  defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
 | 
					  defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do
 | 
				
			||||||
    assigns = %{slug: slug}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    slug_block = ~H"""
 | 
					    slug_block = ~H"""
 | 
				
			||||||
    <.link
 | 
					    <.link navigate={~p"/context/#{@slug}"} class="link">
 | 
				
			||||||
      navigate={Routes.context_show_path(Endpoint, :show, @slug)}
 | 
					 | 
				
			||||||
      class="link"
 | 
					 | 
				
			||||||
      data-qa={"context-show-#{@slug}"}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <%= @slug %>
 | 
					      <%= @slug %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -104,16 +98,12 @@ defmodule MemexWeb.Components.ContextsTableComponent do
 | 
				
			|||||||
    {slug, slug_block}
 | 
					    {slug, slug_block}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
 | 
					  defp get_value_for_key(:tags, assigns, _additional_data) do
 | 
				
			||||||
    assigns = %{tags: tags}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div class="flex flex-wrap justify-center space-x-1">
 | 
					    <div class="flex flex-wrap justify-center space-x-1">
 | 
				
			||||||
      <%= for tag <- @tags do %>
 | 
					      <.link :for={tag <- @tags} patch={~p"/contexts/#{tag}"} class="link">
 | 
				
			||||||
        <.link patch={Routes.context_index_path(Endpoint, :search, tag)} class="link">
 | 
					 | 
				
			||||||
        <%= tag %>
 | 
					        <%= tag %>
 | 
				
			||||||
      </.link>
 | 
					      </.link>
 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										183
									
								
								lib/memex_web/components/core_components.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								lib/memex_web/components/core_components.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,183 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.CoreComponents do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  Provides core UI components.
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  use Phoenix.Component
 | 
				
			||||||
 | 
					  use MemexWeb, :verified_routes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  import MemexWeb.{Gettext, HTMLHelpers}
 | 
				
			||||||
 | 
					  alias Memex.{Accounts, Accounts.Invite, Accounts.User}
 | 
				
			||||||
 | 
					  alias Memex.Contexts.Context
 | 
				
			||||||
 | 
					  alias Memex.Notes.Note
 | 
				
			||||||
 | 
					  alias Memex.Pipelines.Steps.Step
 | 
				
			||||||
 | 
					  alias Phoenix.HTML
 | 
				
			||||||
 | 
					  alias Phoenix.LiveView.JS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates("core_components/*")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :title_content, :string, default: nil
 | 
				
			||||||
 | 
					  attr :current_user, User, default: nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def topbar(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :return_to, :string, required: true
 | 
				
			||||||
 | 
					  slot(:inner_block)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Renders a live component inside a modal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The rendered modal receives a `:return_to` option to properly update
 | 
				
			||||||
 | 
					  the URL when the modal is closed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <.modal return_to={~p"/\#{<%= schema.plural %>}"}>
 | 
				
			||||||
 | 
					        <.live_component
 | 
				
			||||||
 | 
					          module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
 | 
				
			||||||
 | 
					          id={@<%= schema.singular %>.id || :new}
 | 
				
			||||||
 | 
					          title={@page_title}
 | 
				
			||||||
 | 
					          action={@live_action}
 | 
				
			||||||
 | 
					          return_to={~p"/\#{<%= schema.singular %>}"}
 | 
				
			||||||
 | 
					          <%= schema.singular %>: @<%= schema.singular %>
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </.modal>
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  def modal(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp hide_modal(js \\ %JS{}) do
 | 
				
			||||||
 | 
					    js
 | 
				
			||||||
 | 
					    |> JS.hide(to: "#modal", transition: "fade-out")
 | 
				
			||||||
 | 
					    |> JS.hide(to: "#modal-bg", transition: "fade-out")
 | 
				
			||||||
 | 
					    |> JS.hide(to: "#modal-content", transition: "fade-out-scale")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :action, :string, required: true
 | 
				
			||||||
 | 
					  attr :value, :boolean, required: true
 | 
				
			||||||
 | 
					  attr :id, :string, default: nil
 | 
				
			||||||
 | 
					  slot(:inner_block)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  A toggle button element that can be directed to a liveview or a
 | 
				
			||||||
 | 
					  live_component's `handle_event/3`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <.toggle_button action="my_liveview_action" value={@some_value}>
 | 
				
			||||||
 | 
					    <span>Toggle me!</span>
 | 
				
			||||||
 | 
					  </.toggle_button>
 | 
				
			||||||
 | 
					  <.toggle_button action="my_live_component_action" target={@myself} value={@some_value}>
 | 
				
			||||||
 | 
					    <span>Whatever you want</span>
 | 
				
			||||||
 | 
					  </.toggle_button>
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  def toggle_button(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :user, User, required: true
 | 
				
			||||||
 | 
					  slot(:inner_block, required: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def user_card(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :invite, Invite, required: true
 | 
				
			||||||
 | 
					  attr :use_count, :integer, default: nil
 | 
				
			||||||
 | 
					  attr :current_user, User, required: true
 | 
				
			||||||
 | 
					  slot(:inner_block)
 | 
				
			||||||
 | 
					  slot(:code_actions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def invite_card(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :id, :string, required: true
 | 
				
			||||||
 | 
					  attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Phoenix.Component for a <time> element that renders the naivedatetime in the
 | 
				
			||||||
 | 
					  user's local timezone
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  def datetime(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec cast_datetime(NaiveDateTime.t() | nil) :: String.t()
 | 
				
			||||||
 | 
					  defp cast_datetime(%NaiveDateTime{} = datetime) do
 | 
				
			||||||
 | 
					    datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp cast_datetime(_datetime), do: ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :id, :string, required: true
 | 
				
			||||||
 | 
					  attr :date, :any, required: true, doc: "A `Date` struct or nil"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Phoenix.Component for a <date> element that renders the Date in the user's
 | 
				
			||||||
 | 
					  local timezone
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  def date(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :content, :string, required: true
 | 
				
			||||||
 | 
					  attr :filename, :string, default: "qrcode", doc: "filename without .png extension"
 | 
				
			||||||
 | 
					  attr :image_class, :string, default: "w-64 h-max"
 | 
				
			||||||
 | 
					  attr :width, :integer, default: 384, doc: "width of png to generate"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Creates a downloadable QR Code element
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  def qr_code(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :note, Note, required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def note_content(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :context, Context, required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def context_content(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attr :step, Step, required: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def step_content(assigns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp add_links_to_content(content, data_qa_prefix) do
 | 
				
			||||||
 | 
					    # replace links
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # link regex from
 | 
				
			||||||
 | 
					    # https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
 | 
				
			||||||
 | 
					    # and modified with additional schemes from
 | 
				
			||||||
 | 
					    # https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    content =
 | 
				
			||||||
 | 
					      Regex.replace(
 | 
				
			||||||
 | 
					        ~r<((file|git|https?|ipfs|ipns|irc|jabber|magnet|mailto|mumble|tel|udp|xmpp):\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))>,
 | 
				
			||||||
 | 
					        content,
 | 
				
			||||||
 | 
					        fn _whole_match, link ->
 | 
				
			||||||
 | 
					          link =
 | 
				
			||||||
 | 
					            HTML.Link.link(
 | 
				
			||||||
 | 
					              link,
 | 
				
			||||||
 | 
					              to: link,
 | 
				
			||||||
 | 
					              class: "link inline",
 | 
				
			||||||
 | 
					              target: "_blank",
 | 
				
			||||||
 | 
					              rel: "noopener noreferrer"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            |> HTML.Safe.to_iodata()
 | 
				
			||||||
 | 
					            |> IO.iodata_to_binary()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          "</p>#{link}<p class=\"inline\">"
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    content =
 | 
				
			||||||
 | 
					      Regex.replace(
 | 
				
			||||||
 | 
					        ~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
 | 
				
			||||||
 | 
					        content,
 | 
				
			||||||
 | 
					        fn _whole_match, slug ->
 | 
				
			||||||
 | 
					          link =
 | 
				
			||||||
 | 
					            HTML.Link.link(
 | 
				
			||||||
 | 
					              "[[#{slug}]]",
 | 
				
			||||||
 | 
					              to: ~p"/note/#{slug}",
 | 
				
			||||||
 | 
					              class: "link inline",
 | 
				
			||||||
 | 
					              data: [qa: "#{data_qa_prefix}-#{slug}"]
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            |> HTML.Safe.to_iodata()
 | 
				
			||||||
 | 
					            |> IO.iodata_to_binary()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          "</p>#{link}<p class=\"inline\">"
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    content |> HTML.raw()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<div
 | 
				
			||||||
 | 
					  id={"show-context-content-#{@context.id}"}
 | 
				
			||||||
 | 
					  class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
 | 
				
			||||||
 | 
					  phx-hook="MaintainAttrs"
 | 
				
			||||||
 | 
					  phx-update="ignore"
 | 
				
			||||||
 | 
					  readonly
 | 
				
			||||||
 | 
					  phx-no-format
 | 
				
			||||||
 | 
					><p class="inline"><%= add_links_to_content(@context.content, "context-note") %></p></div>
 | 
				
			||||||
							
								
								
									
										3
									
								
								lib/memex_web/components/core_components/date.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/memex_web/components/core_components/date.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<time :if={@date} id={@id} datetime={Date.to_iso8601(@date, :extended)} phx-hook="Date">
 | 
				
			||||||
 | 
					  <%= Date.to_iso8601(@date, :extended) %>
 | 
				
			||||||
 | 
					</time>
 | 
				
			||||||
@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<time :if={@datetime} id={@id} datetime={cast_datetime(@datetime)} phx-hook="DateTime">
 | 
				
			||||||
 | 
					  <%= cast_datetime(@datetime) %>
 | 
				
			||||||
 | 
					</time>
 | 
				
			||||||
@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					<div class="px-8 py-4 flex flex-col justify-center items-center space-y-4
 | 
				
			||||||
 | 
					  bg-primary-900
 | 
				
			||||||
 | 
					  border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
				
			||||||
 | 
					  transition-all duration-300 ease-in-out">
 | 
				
			||||||
 | 
					  <h1 class="title text-xl">
 | 
				
			||||||
 | 
					    <%= @invite.name %>
 | 
				
			||||||
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <%= if @invite.disabled_at |> is_nil() do %>
 | 
				
			||||||
 | 
					    <h2 class="title text-md">
 | 
				
			||||||
 | 
					      <%= if @invite.uses_left do %>
 | 
				
			||||||
 | 
					        <%= gettext(
 | 
				
			||||||
 | 
					          "uses left: %{uses_left_count}",
 | 
				
			||||||
 | 
					          uses_left_count: @invite.uses_left
 | 
				
			||||||
 | 
					        ) %>
 | 
				
			||||||
 | 
					      <% else %>
 | 
				
			||||||
 | 
					        <%= gettext("uses left: unlimited") %>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </h2>
 | 
				
			||||||
 | 
					  <% else %>
 | 
				
			||||||
 | 
					    <h2 class="title text-md">
 | 
				
			||||||
 | 
					      <%= gettext("invite disabled") %>
 | 
				
			||||||
 | 
					    </h2>
 | 
				
			||||||
 | 
					  <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <.qr_code
 | 
				
			||||||
 | 
					    content={url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}")}
 | 
				
			||||||
 | 
					    filename={@invite.name}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <h2 :if={@use_count && @use_count != 0} class="title text-md">
 | 
				
			||||||
 | 
					    <%= gettext("uses: %{uses_count}", uses_count: @use_count) %>
 | 
				
			||||||
 | 
					  </h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="flex flex-row flex-wrap justify-center items-center">
 | 
				
			||||||
 | 
					    <code
 | 
				
			||||||
 | 
					      id={"code-#{@invite.id}"}
 | 
				
			||||||
 | 
					      class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all
 | 
				
			||||||
 | 
					        text-primary-400 bg-primary-800"
 | 
				
			||||||
 | 
					      phx-no-format
 | 
				
			||||||
 | 
					    ><%= url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %></code>
 | 
				
			||||||
 | 
					    <%= if @code_actions, do: render_slot(@code_actions) %>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div :if={@inner_block} class="flex space-x-4 justify-center items-center">
 | 
				
			||||||
 | 
					    <%= render_slot(@inner_block) %>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										43
									
								
								lib/memex_web/components/core_components/modal.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								lib/memex_web/components/core_components/modal.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					<.link
 | 
				
			||||||
 | 
					  id="modal-bg"
 | 
				
			||||||
 | 
					  patch={@return_to}
 | 
				
			||||||
 | 
					  class="fade-in fixed z-10 left-0 top-0
 | 
				
			||||||
 | 
					    w-full h-full overflow-hidden
 | 
				
			||||||
 | 
					    p-8 flex flex-col justify-center items-center cursor-auto"
 | 
				
			||||||
 | 
					  style="background-color: rgba(0,0,0,0.4);"
 | 
				
			||||||
 | 
					  phx-remove={hide_modal()}
 | 
				
			||||||
 | 
					  aria-label={gettext("close modal")}
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					  <span class="hidden"></span>
 | 
				
			||||||
 | 
					</.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div
 | 
				
			||||||
 | 
					  id="modal"
 | 
				
			||||||
 | 
					  class="fixed z-10 left-0 top-0 pointer-events-none
 | 
				
			||||||
 | 
					    w-full h-full overflow-hidden
 | 
				
			||||||
 | 
					    p-4 sm:p-8 flex flex-col justify-center items-center"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    id="modal-content"
 | 
				
			||||||
 | 
					    class="fade-in-scale max-w-3xl max-h-3xl relative w-full
 | 
				
			||||||
 | 
					      pointer-events-auto overflow-hidden
 | 
				
			||||||
 | 
					      px-8 py-4 sm:py-8 flex flex-col justify-start items-stretch
 | 
				
			||||||
 | 
					      bg-primary-800 text-primary-400 border-primary-900 border-2 rounded-lg"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <.link
 | 
				
			||||||
 | 
					      patch={@return_to}
 | 
				
			||||||
 | 
					      id="close"
 | 
				
			||||||
 | 
					      class="absolute top-8 right-10
 | 
				
			||||||
 | 
					        text-gray-500 hover:text-gray-800
 | 
				
			||||||
 | 
					        transition-all duration-500 ease-in-out"
 | 
				
			||||||
 | 
					      phx-remove={hide_modal()}
 | 
				
			||||||
 | 
					      aria-label={gettext("close modal")}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <i class="fa-fw fa-lg fas fa-times"></i>
 | 
				
			||||||
 | 
					    </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-stretch">
 | 
				
			||||||
 | 
					      <%= render_slot(@inner_block) %>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<div
 | 
				
			||||||
 | 
					  id={"show-note-content-#{@note.id}"}
 | 
				
			||||||
 | 
					  class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
 | 
				
			||||||
 | 
					  phx-hook="MaintainAttrs"
 | 
				
			||||||
 | 
					  phx-update="ignore"
 | 
				
			||||||
 | 
					  readonly
 | 
				
			||||||
 | 
					  phx-no-format
 | 
				
			||||||
 | 
					><p class="inline"><%= add_links_to_content(@note.content, "note-link") %></p></div>
 | 
				
			||||||
@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					<a href={qr_code_image(@content)} download={@filename <> ".png"}>
 | 
				
			||||||
 | 
					  <img class={@image_class} alt={@filename} src={qr_code_image(@content)} />
 | 
				
			||||||
 | 
					</a>
 | 
				
			||||||
@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<div
 | 
				
			||||||
 | 
					  id={"show-step-content-#{@step.id}"}
 | 
				
			||||||
 | 
					  class="input input-primary h-32 min-h-32 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
 | 
				
			||||||
 | 
					  phx-hook="MaintainAttrs"
 | 
				
			||||||
 | 
					  phx-update="ignore"
 | 
				
			||||||
 | 
					  readonly
 | 
				
			||||||
 | 
					  phx-no-format
 | 
				
			||||||
 | 
					><p class="inline"><%= add_links_to_content(@step.content, "step-context") %></p></div>
 | 
				
			||||||
@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					<label for={@id || @action} class="relative inline-flex items-center cursor-pointer">
 | 
				
			||||||
 | 
					  <input
 | 
				
			||||||
 | 
					    id={@id || @action}
 | 
				
			||||||
 | 
					    type="checkbox"
 | 
				
			||||||
 | 
					    value={@value}
 | 
				
			||||||
 | 
					    checked={@value}
 | 
				
			||||||
 | 
					    class="sr-only peer"
 | 
				
			||||||
 | 
					    aria-labelledby={"#{@id || @action}-label"}
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      if assigns |> Map.has_key?(:target),
 | 
				
			||||||
 | 
					        do: %{"phx-click": @action, "phx-value-value": @value, "phx-target": @target},
 | 
				
			||||||
 | 
					        else: %{"phx-click": @action, "phx-value-value": @value}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					  <div class="w-11 h-6 bg-gray-300 rounded-full peer
 | 
				
			||||||
 | 
					    peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
 | 
				
			||||||
 | 
					    peer-checked:bg-gray-600
 | 
				
			||||||
 | 
					    peer-checked:after:translate-x-full peer-checked:after:border-white
 | 
				
			||||||
 | 
					    after:content-[''] after:absolute after:top-1 after:left-[2px] after:bg-white after:border-gray-300
 | 
				
			||||||
 | 
					    after:border after:rounded-full after:h-5 after:w-5
 | 
				
			||||||
 | 
					    after:transition-all after:duration-250 after:ease-in-out
 | 
				
			||||||
 | 
					    transition-colors duration-250 ease-in-out">
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <span
 | 
				
			||||||
 | 
					    id={"#{@id || @action}-label"}
 | 
				
			||||||
 | 
					    class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300 whitespace-nowrap"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <%= render_slot(@inner_block) %>
 | 
				
			||||||
 | 
					  </span>
 | 
				
			||||||
 | 
					</label>
 | 
				
			||||||
							
								
								
									
										91
									
								
								lib/memex_web/components/core_components/topbar.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								lib/memex_web/components/core_components/topbar.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
				
			|||||||
 | 
					<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-900 text-primary-400">
 | 
				
			||||||
 | 
					  <div class="flex flex-col sm:flex-row justify-between items-center">
 | 
				
			||||||
 | 
					    <div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
 | 
				
			||||||
 | 
					      <.link navigate={~p"/"} class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline">
 | 
				
			||||||
 | 
					        <%= gettext("memEx") %>
 | 
				
			||||||
 | 
					      </.link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <%= if @title_content do %>
 | 
				
			||||||
 | 
					        <span class="mx-2 my-1">
 | 
				
			||||||
 | 
					          |
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        <%= @title_content %>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <hr class="mb-2 sm:hidden hr-light" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ul class="flex flex-row flex-wrap justify-center items-center
 | 
				
			||||||
 | 
					      text-lg text-primary-400 text-ellipsis">
 | 
				
			||||||
 | 
					      <li class="mx-2 my-1">
 | 
				
			||||||
 | 
					        <.link navigate={~p"/notes"} class="text-primary-400 hover:underline truncate">
 | 
				
			||||||
 | 
					          <%= gettext("notes") %>
 | 
				
			||||||
 | 
					        </.link>
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <li class="mx-2 my-1">
 | 
				
			||||||
 | 
					        <.link navigate={~p"/contexts"} class="text-primary-400 hover:underline truncate">
 | 
				
			||||||
 | 
					          <%= gettext("contexts") %>
 | 
				
			||||||
 | 
					        </.link>
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <li class="mx-2 my-1">
 | 
				
			||||||
 | 
					        <.link navigate={~p"/pipelines"} class="text-primary-400 hover:underline truncate">
 | 
				
			||||||
 | 
					          <%= gettext("pipelines") %>
 | 
				
			||||||
 | 
					        </.link>
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <li class="mx-2 my-1 border-left border border-primary-700"></li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <%= if @current_user do %>
 | 
				
			||||||
 | 
					        <li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1">
 | 
				
			||||||
 | 
					          <.link navigate={~p"/invites"} class="text-primary-400 hover:underline">
 | 
				
			||||||
 | 
					            <%= gettext("invites") %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <li class="mx-2 my-1">
 | 
				
			||||||
 | 
					          <.link navigate={~p"/users/settings"} class="text-primary-400 hover:underline truncate">
 | 
				
			||||||
 | 
					            <%= @current_user.email %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					        <li class="mx-2 my-1">
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            href={~p"/users/log_out"}
 | 
				
			||||||
 | 
					            method="delete"
 | 
				
			||||||
 | 
					            data-confirm={dgettext("prompts", "are you sure you want to log out?")}
 | 
				
			||||||
 | 
					            aria-label={gettext("log out")}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fas fa-sign-out-alt"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					        <li
 | 
				
			||||||
 | 
					          :if={
 | 
				
			||||||
 | 
					            @current_user.role == :admin and function_exported?(Routes, :live_dashboard_path, 2)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          class="mx-2 my-1"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <.link
 | 
				
			||||||
 | 
					            navigate={~p"/dashboard"}
 | 
				
			||||||
 | 
					            class="text-primary-400 hover:underline"
 | 
				
			||||||
 | 
					            aria-label={gettext("live dashboard")}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <i class="fas fa-gauge"></i>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					      <% else %>
 | 
				
			||||||
 | 
					        <li :if={Accounts.allow_registration?()} class="mx-2 my-1">
 | 
				
			||||||
 | 
					          <.link href={~p"/users/register"} class="text-primary-400 hover:underline truncate">
 | 
				
			||||||
 | 
					            <%= dgettext("actions", "register") %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <li class="mx-2 my-1">
 | 
				
			||||||
 | 
					          <.link href={~p"/users/log_in"} class="text-primary-400 hover:underline truncate">
 | 
				
			||||||
 | 
					            <%= dgettext("actions", "log in") %>
 | 
				
			||||||
 | 
					          </.link>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </ul>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</nav>
 | 
				
			||||||
							
								
								
									
										37
									
								
								lib/memex_web/components/core_components/user_card.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								lib/memex_web/components/core_components/user_card.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					<div
 | 
				
			||||||
 | 
					  id={"user-#{@user.id}"}
 | 
				
			||||||
 | 
					  class="px-8 py-4 flex flex-col justify-center items-center text-center
 | 
				
			||||||
 | 
					    bg-primary-900
 | 
				
			||||||
 | 
					    border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
				
			||||||
 | 
					    transition-all duration-300 ease-in-out"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					  <h1 class="px-4 py-2 rounded-lg title text-xl break-all">
 | 
				
			||||||
 | 
					    <%= @user.email %>
 | 
				
			||||||
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      <%= if @user.confirmed_at do %>
 | 
				
			||||||
 | 
					        <%= gettext(
 | 
				
			||||||
 | 
					          "user confirmed on%{confirmed_datetime}",
 | 
				
			||||||
 | 
					          confirmed_datetime: ""
 | 
				
			||||||
 | 
					        ) %>
 | 
				
			||||||
 | 
					        <.datetime id={"#{@user.id}-confirmed-at"} datetime={@user.confirmed_at} />
 | 
				
			||||||
 | 
					      <% else %>
 | 
				
			||||||
 | 
					        <%= gettext("email unconfirmed") %>
 | 
				
			||||||
 | 
					      <% end %>
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      <%= gettext(
 | 
				
			||||||
 | 
					        "user registered on%{registered_datetime}",
 | 
				
			||||||
 | 
					        registered_datetime: ""
 | 
				
			||||||
 | 
					      ) %>
 | 
				
			||||||
 | 
					      <.datetime id={"#{@user.id}-inserted-at"} datetime={@user.inserted_at} />
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  </h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
 | 
				
			||||||
 | 
					    <%= render_slot(@inner_block) %>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,49 +0,0 @@
 | 
				
			|||||||
defmodule MemexWeb.Components.InviteCard do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Display card for an invite
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  use MemexWeb, :component
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def invite_card(assigns) do
 | 
					 | 
				
			||||||
    ~H"""
 | 
					 | 
				
			||||||
    <div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
 | 
					 | 
				
			||||||
      border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
					 | 
				
			||||||
      transition-all duration-300 ease-in-out">
 | 
					 | 
				
			||||||
      <h1 class="title text-xl">
 | 
					 | 
				
			||||||
        <%= @invite.name %>
 | 
					 | 
				
			||||||
      </h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= if @invite.disabled_at |> is_nil() do %>
 | 
					 | 
				
			||||||
        <h2 class="title text-md">
 | 
					 | 
				
			||||||
          <%= gettext("Uses Left:") %>
 | 
					 | 
				
			||||||
          <%= @invite.uses_left || gettext("unlimited") %>
 | 
					 | 
				
			||||||
        </h2>
 | 
					 | 
				
			||||||
      <% else %>
 | 
					 | 
				
			||||||
        <h2 class="title text-md">
 | 
					 | 
				
			||||||
          <%= gettext("invite disabled") %>
 | 
					 | 
				
			||||||
        </h2>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="flex flex-row flex-wrap justify-center items-center">
 | 
					 | 
				
			||||||
        <code
 | 
					 | 
				
			||||||
          id={"code-#{@invite.id}"}
 | 
					 | 
				
			||||||
          class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %>
 | 
					 | 
				
			||||||
        </code>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <%= if @code_actions do %>
 | 
					 | 
				
			||||||
          <%= render_slot(@code_actions) %>
 | 
					 | 
				
			||||||
        <% end %>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= if @inner_block do %>
 | 
					 | 
				
			||||||
        <div class="flex space-x-4 justify-center items-center">
 | 
					 | 
				
			||||||
          <%= render_slot(@inner_block) %>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
							
								
								
									
										17
									
								
								lib/memex_web/components/layouts.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/memex_web/components/layouts.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.Layouts do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  The root layouts for the entire application
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "layouts/*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def get_title(%{assigns: %{title: title}}) when title not in [nil, ""] do
 | 
				
			||||||
 | 
					    gettext("memEx | %{title}", title: title)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def get_title(_conn) do
 | 
				
			||||||
 | 
					    gettext("memEx")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										18
									
								
								lib/memex_web/components/layouts/app.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								lib/memex_web/components/layouts/app.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					<main role="main" class="min-h-full min-w-full">
 | 
				
			||||||
 | 
					  <header>
 | 
				
			||||||
 | 
					    <.topbar current_user={assigns[:current_user]} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="mx-8 my-2 flex flex-col space-y-4 text-center">
 | 
				
			||||||
 | 
					      <p :if={@flash["info"]} class="alert alert-info" role="alert">
 | 
				
			||||||
 | 
					        <%= @flash["info"] %>
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					      <p :if={@flash["error"]} class="alert alert-danger" role="alert">
 | 
				
			||||||
 | 
					        <%= @flash["error"] %>
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="mx-4 sm:mx-8 md:mx-16">
 | 
				
			||||||
 | 
					    <%= @inner_content %>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
@@ -9,11 +9,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <hr style="margin: 2em auto; border-width: 1px; border-color: rgb(161, 161, 170); width: 100%; max-width: 42rem;" />
 | 
					    <hr style="margin: 2em auto; border-width: 1px; border-color: rgb(161, 161, 170); width: 100%; max-width: 42rem;" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <a style="color: rgb(161, 161, 170);" href={Routes.live_url(Endpoint, HomeLive)}>
 | 
					    <a style="color: rgb(161, 161, 170);" href={~p"/"}>
 | 
				
			||||||
      <%= dgettext(
 | 
					      <%= dgettext("emails", "this email was sent from memEx") %>
 | 
				
			||||||
        "emails",
 | 
					 | 
				
			||||||
        "This email was sent from memEx"
 | 
					 | 
				
			||||||
      ) %>
 | 
					 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										9
									
								
								lib/memex_web/components/layouts/email_text.txt.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/memex_web/components/layouts/email_text.txt.eex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<%= @email.subject %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					====================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%= @inner_content %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					=====================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%= dgettext("emails", "this email was sent from memEx at %{url}", url: ~p"/") %>
 | 
				
			||||||
							
								
								
									
										45
									
								
								lib/memex_web/components/layouts/live.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								lib/memex_web/components/layouts/live.html.heex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					<main class="pb-8 min-w-full">
 | 
				
			||||||
 | 
					  <header>
 | 
				
			||||||
 | 
					    <.topbar current_user={assigns[:current_user]} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="mx-8 my-2 flex flex-col space-y-4 text-center">
 | 
				
			||||||
 | 
					      <p
 | 
				
			||||||
 | 
					        :if={@flash && @flash |> Map.has_key?("info")}
 | 
				
			||||||
 | 
					        class="alert alert-info"
 | 
				
			||||||
 | 
					        role="alert"
 | 
				
			||||||
 | 
					        phx-click="lv:clear-flash"
 | 
				
			||||||
 | 
					        phx-value-key="info"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= live_flash(@flash, "info") %>
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <p
 | 
				
			||||||
 | 
					        :if={@flash && @flash |> Map.has_key?("error")}
 | 
				
			||||||
 | 
					        class="alert alert-danger"
 | 
				
			||||||
 | 
					        role="alert"
 | 
				
			||||||
 | 
					        phx-click="lv:clear-flash"
 | 
				
			||||||
 | 
					        phx-value-key="error"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <%= live_flash(@flash, "error") %>
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="mx-4 sm:mx-8 md:mx-16">
 | 
				
			||||||
 | 
					    <%= @inner_content %>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div
 | 
				
			||||||
 | 
					  id="disconnect"
 | 
				
			||||||
 | 
					  class="z-50 fixed opacity-0 bottom-12 right-12 px-8 py-4 w-max h-max
 | 
				
			||||||
 | 
					  border border-primary-400 shadow-lg rounded-lg bg-primary-900 text-primary-400
 | 
				
			||||||
 | 
					  flex justify-center items-center space-x-4
 | 
				
			||||||
 | 
					  transition-opacity ease-in-out duration-500 delay-[2000ms]"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					  <i class="fas fa-fade text-md fa-satellite-dish"></i>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <h1 class="title text-md">
 | 
				
			||||||
 | 
					    <%= gettext("Reconnecting...") %>
 | 
				
			||||||
 | 
					  </h1>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html lang="en" class="m-0 p-0 w-full h-full">
 | 
					<html lang="en" class="m-0 p-0 w-full h-full bg-primary-800">
 | 
				
			||||||
  <head>
 | 
					  <head>
 | 
				
			||||||
    <meta charset="utf-8" />
 | 
					    <meta charset="utf-8" />
 | 
				
			||||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
					    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
				
			||||||
@@ -8,17 +8,12 @@
 | 
				
			|||||||
    <.live_title suffix={" | #{gettext("memEx")}"}>
 | 
					    <.live_title suffix={" | #{gettext("memEx")}"}>
 | 
				
			||||||
      <%= assigns[:page_title] || gettext("memEx") %>
 | 
					      <%= assigns[:page_title] || gettext("memEx") %>
 | 
				
			||||||
    </.live_title>
 | 
					    </.live_title>
 | 
				
			||||||
    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
 | 
					    <link phx-track-static rel="stylesheet" href={~p"/css/app.css"} />
 | 
				
			||||||
    <script
 | 
					    <script defer phx-track-static type="text/javascript" src={~p"/js/app.js"}>
 | 
				
			||||||
      defer
 | 
					 | 
				
			||||||
      phx-track-static
 | 
					 | 
				
			||||||
      type="text/javascript"
 | 
					 | 
				
			||||||
      src={Routes.static_path(@conn, "/js/app.js")}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <body class="m-0 p-0 w-full h-full bg-primary-800 text-primary-400 subpixel-antialiased">
 | 
					  <body class="m-0 p-0 w-full h-full text-primary-400 subpixel-antialiased">
 | 
				
			||||||
    <%= @inner_content %>
 | 
					    <%= @inner_content %>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
@@ -1,45 +0,0 @@
 | 
				
			|||||||
defmodule MemexWeb.Components.NoteContent do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Display the content for a note
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  use MemexWeb, :component
 | 
					 | 
				
			||||||
  alias Memex.Notes.Note
 | 
					 | 
				
			||||||
  alias Phoenix.HTML
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  attr :note, Note, required: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def note_content(assigns) do
 | 
					 | 
				
			||||||
    ~H"""
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      id={"show-note-content-#{@note.id}"}
 | 
					 | 
				
			||||||
      class="input input-primary h-128 min-h-128 inline-block overflow-x-hidden overflow-y-auto"
 | 
					 | 
				
			||||||
      phx-hook="MaintainAttrs"
 | 
					 | 
				
			||||||
      phx-update="ignore"
 | 
					 | 
				
			||||||
      readonly
 | 
					 | 
				
			||||||
      phx-no-format
 | 
					 | 
				
			||||||
    ><p class="inline"><%= add_links_to_content(@note.content) %></p></div>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp add_links_to_content(content) do
 | 
					 | 
				
			||||||
    Regex.replace(
 | 
					 | 
				
			||||||
      ~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
 | 
					 | 
				
			||||||
      content,
 | 
					 | 
				
			||||||
      fn _whole_match, slug ->
 | 
					 | 
				
			||||||
        link =
 | 
					 | 
				
			||||||
          HTML.Link.link(
 | 
					 | 
				
			||||||
            "[[#{slug}]]",
 | 
					 | 
				
			||||||
            to: Routes.note_show_path(Endpoint, :show, slug),
 | 
					 | 
				
			||||||
            class: "link inline",
 | 
					 | 
				
			||||||
            data: [qa: "note-link-#{slug}"]
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
          |> HTML.Safe.to_iodata()
 | 
					 | 
				
			||||||
          |> IO.iodata_to_binary()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        "</p>#{link}<p class=\"inline\">"
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    |> String.replace("\n", "<br>")
 | 
					 | 
				
			||||||
    |> HTML.raw()
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@@ -40,7 +40,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
 | 
				
			|||||||
      if actions == [] or current_user |> is_nil() do
 | 
					      if actions == [] or current_user |> is_nil() do
 | 
				
			||||||
        []
 | 
					        []
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        [%{label: nil, key: :actions, sortable: false}]
 | 
					        [%{label: gettext("actions"), key: :actions, sortable: false}]
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    columns = [
 | 
					    columns = [
 | 
				
			||||||
@@ -92,11 +92,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
 | 
				
			|||||||
    assigns = %{slug: slug}
 | 
					    assigns = %{slug: slug}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    slug_block = ~H"""
 | 
					    slug_block = ~H"""
 | 
				
			||||||
    <.link
 | 
					    <.link navigate={~p"/note/#{@slug}"} class="link">
 | 
				
			||||||
      navigate={Routes.note_show_path(Endpoint, :show, @slug)}
 | 
					 | 
				
			||||||
      class="link"
 | 
					 | 
				
			||||||
      data-qa={"note-show-#{@slug}"}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <%= @slug %>
 | 
					      <%= @slug %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -109,11 +105,9 @@ defmodule MemexWeb.Components.NotesTableComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div class="flex flex-wrap justify-center space-x-1">
 | 
					    <div class="flex flex-wrap justify-center space-x-1">
 | 
				
			||||||
      <%= for tag <- @tags do %>
 | 
					      <.link :for={tag <- @tags} patch={~p"/notes/#{tag}"} class="link">
 | 
				
			||||||
        <.link patch={Routes.note_index_path(Endpoint, :search, tag)} class="link">
 | 
					 | 
				
			||||||
        <%= tag %>
 | 
					        <%= tag %>
 | 
				
			||||||
      </.link>
 | 
					      </.link>
 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
 | 
				
			|||||||
      if actions == [] or current_user |> is_nil() do
 | 
					      if actions == [] or current_user |> is_nil() do
 | 
				
			||||||
        []
 | 
					        []
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        [%{label: nil, key: :actions, sortable: false}]
 | 
					        [%{label: gettext("actions"), key: :actions, sortable: false}]
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    columns = [
 | 
					    columns = [
 | 
				
			||||||
@@ -93,11 +93,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
 | 
				
			|||||||
    assigns = %{slug: slug}
 | 
					    assigns = %{slug: slug}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    slug_block = ~H"""
 | 
					    slug_block = ~H"""
 | 
				
			||||||
    <.link
 | 
					    <.link navigate={~p"/pipeline/#{@slug}"} class="link">
 | 
				
			||||||
      navigate={Routes.pipeline_show_path(Endpoint, :show, @slug)}
 | 
					 | 
				
			||||||
      class="link"
 | 
					 | 
				
			||||||
      data-qa={"pipeline-show-#{@slug}"}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <%= @slug %>
 | 
					      <%= @slug %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -122,11 +118,9 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    ~H"""
 | 
					    ~H"""
 | 
				
			||||||
    <div class="flex flex-wrap justify-center space-x-1">
 | 
					    <div class="flex flex-wrap justify-center space-x-1">
 | 
				
			||||||
      <%= for tag <- @tags do %>
 | 
					      <.link :for={tag <- @tags} patch={~p"/pipelines/#{tag}"} class="link">
 | 
				
			||||||
        <.link patch={Routes.pipeline_index_path(Endpoint, :search, tag)} class="link">
 | 
					 | 
				
			||||||
        <%= tag %>
 | 
					        <%= tag %>
 | 
				
			||||||
      </.link>
 | 
					      </.link>
 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,45 +0,0 @@
 | 
				
			|||||||
defmodule MemexWeb.Components.StepContent do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Display the content for a step
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
  use MemexWeb, :component
 | 
					 | 
				
			||||||
  alias Memex.Pipelines.Steps.Step
 | 
					 | 
				
			||||||
  alias Phoenix.HTML
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  attr :step, Step, required: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def step_content(assigns) do
 | 
					 | 
				
			||||||
    ~H"""
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      id={"show-step-content-#{@step.id}"}
 | 
					 | 
				
			||||||
      class="input input-primary h-32 min-h-32 inline-block overflow-x-hidden overflow-y-auto"
 | 
					 | 
				
			||||||
      phx-hook="MaintainAttrs"
 | 
					 | 
				
			||||||
      phx-update="ignore"
 | 
					 | 
				
			||||||
      readonly
 | 
					 | 
				
			||||||
      phx-no-format
 | 
					 | 
				
			||||||
    ><p class="inline"><%= add_links_to_content(@step.content) %></p></div>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  defp add_links_to_content(content) do
 | 
					 | 
				
			||||||
    Regex.replace(
 | 
					 | 
				
			||||||
      ~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
 | 
					 | 
				
			||||||
      content,
 | 
					 | 
				
			||||||
      fn _whole_match, slug ->
 | 
					 | 
				
			||||||
        link =
 | 
					 | 
				
			||||||
          HTML.Link.link(
 | 
					 | 
				
			||||||
            "[[#{slug}]]",
 | 
					 | 
				
			||||||
            to: Routes.context_show_path(Endpoint, :show, slug),
 | 
					 | 
				
			||||||
            class: "link inline",
 | 
					 | 
				
			||||||
            data: [qa: "step-context-#{slug}"]
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
          |> HTML.Safe.to_iodata()
 | 
					 | 
				
			||||||
          |> IO.iodata_to_binary()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        "</p>#{link}<p class=\"inline\">"
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    |> String.replace("\n", "<br>")
 | 
					 | 
				
			||||||
    |> HTML.raw()
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@@ -33,7 +33,8 @@ defmodule MemexWeb.Components.TableComponent do
 | 
				
			|||||||
                optional(:class) => String.t(),
 | 
					                optional(:class) => String.t(),
 | 
				
			||||||
                optional(:row_class) => String.t(),
 | 
					                optional(:row_class) => String.t(),
 | 
				
			||||||
                optional(:alternate_row_class) => String.t(),
 | 
					                optional(:alternate_row_class) => String.t(),
 | 
				
			||||||
                optional(:sortable) => false
 | 
					                optional(:sortable) => false,
 | 
				
			||||||
 | 
					                optional(:type) => module()
 | 
				
			||||||
              }),
 | 
					              }),
 | 
				
			||||||
            required(:rows) =>
 | 
					            required(:rows) =>
 | 
				
			||||||
              list(%{
 | 
					              list(%{
 | 
				
			||||||
@@ -60,7 +61,8 @@ defmodule MemexWeb.Components.TableComponent do
 | 
				
			|||||||
        :asc
 | 
					        :asc
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode)
 | 
					    type = columns |> Enum.find(%{}, fn %{key: key} -> key == initial_key end) |> Map.get(:type)
 | 
				
			||||||
 | 
					    rows = rows |> sort_by_custom_sort_value_or_value(initial_key, initial_sort_mode, type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    socket =
 | 
					    socket =
 | 
				
			||||||
      socket
 | 
					      socket
 | 
				
			||||||
@@ -68,6 +70,7 @@ defmodule MemexWeb.Components.TableComponent do
 | 
				
			|||||||
      |> assign(
 | 
					      |> assign(
 | 
				
			||||||
        columns: columns,
 | 
					        columns: columns,
 | 
				
			||||||
        rows: rows,
 | 
					        rows: rows,
 | 
				
			||||||
 | 
					        key: initial_key,
 | 
				
			||||||
        last_sort_key: initial_key,
 | 
					        last_sort_key: initial_key,
 | 
				
			||||||
        sort_mode: initial_sort_mode
 | 
					        sort_mode: initial_sort_mode
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
@@ -81,7 +84,14 @@ defmodule MemexWeb.Components.TableComponent do
 | 
				
			|||||||
  def handle_event(
 | 
					  def handle_event(
 | 
				
			||||||
        "sort_by",
 | 
					        "sort_by",
 | 
				
			||||||
        %{"sort-key" => key},
 | 
					        %{"sort-key" => key},
 | 
				
			||||||
        %{assigns: %{rows: rows, last_sort_key: last_sort_key, sort_mode: sort_mode}} = socket
 | 
					        %{
 | 
				
			||||||
 | 
					          assigns: %{
 | 
				
			||||||
 | 
					            columns: columns,
 | 
				
			||||||
 | 
					            rows: rows,
 | 
				
			||||||
 | 
					            last_sort_key: last_sort_key,
 | 
				
			||||||
 | 
					            sort_mode: sort_mode
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } = socket
 | 
				
			||||||
      ) do
 | 
					      ) do
 | 
				
			||||||
    key = key |> String.to_existing_atom()
 | 
					    key = key |> String.to_existing_atom()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -92,11 +102,28 @@ defmodule MemexWeb.Components.TableComponent do
 | 
				
			|||||||
        {_new_sort_key, _last_sort_mode} -> :asc
 | 
					        {_new_sort_key, _last_sort_mode} -> :asc
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode)
 | 
					    type =
 | 
				
			||||||
 | 
					      columns |> Enum.find(%{}, fn %{key: column_key} -> column_key == key end) |> Map.get(:type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode, type)
 | 
				
			||||||
    {:noreply, socket |> assign(last_sort_key: key, sort_mode: sort_mode, rows: rows)}
 | 
					    {:noreply, socket |> assign(last_sort_key: key, sort_mode: sort_mode, rows: rows)}
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp sort_by_custom_sort_value_or_value(rows, key, sort_mode) do
 | 
					  defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, type)
 | 
				
			||||||
 | 
					       when type in [Date, DateTime] do
 | 
				
			||||||
 | 
					    rows
 | 
				
			||||||
 | 
					    |> Enum.sort_by(
 | 
				
			||||||
 | 
					      fn row ->
 | 
				
			||||||
 | 
					        case row |> Map.get(key) do
 | 
				
			||||||
 | 
					          {custom_sort_key, _value} -> custom_sort_key
 | 
				
			||||||
 | 
					          value -> value
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end,
 | 
				
			||||||
 | 
					      {sort_mode, type}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp sort_by_custom_sort_value_or_value(rows, key, sort_mode, _type) do
 | 
				
			||||||
    rows
 | 
					    rows
 | 
				
			||||||
    |> Enum.sort_by(
 | 
					    |> Enum.sort_by(
 | 
				
			||||||
      fn row ->
 | 
					      fn row ->
 | 
				
			||||||
@@ -108,4 +135,25 @@ defmodule MemexWeb.Components.TableComponent do
 | 
				
			|||||||
      sort_mode
 | 
					      sort_mode
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Conditionally composes elements into the columns list, supports maps and
 | 
				
			||||||
 | 
					  lists. Works tail to front in order for efficiency
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      iex> []
 | 
				
			||||||
 | 
					      ...> |> maybe_compose_columns(%{label: "Column 3"}, true)
 | 
				
			||||||
 | 
					      ...> |> maybe_compose_columns(%{label: "Column 2"}, false)
 | 
				
			||||||
 | 
					      ...> |> maybe_compose_columns(%{label: "Column 1"})
 | 
				
			||||||
 | 
					      [%{label: "Column 1"}, %{label: "Column 3"}]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  @spec maybe_compose_columns(list(), element_to_add :: list() | map()) :: list()
 | 
				
			||||||
 | 
					  @spec maybe_compose_columns(list(), element_to_add :: list() | map(), boolean()) :: list()
 | 
				
			||||||
 | 
					  def maybe_compose_columns(columns, element_or_elements, add? \\ true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def maybe_compose_columns(columns, elements, true) when is_list(elements),
 | 
				
			||||||
 | 
					    do: Enum.concat(elements, columns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def maybe_compose_columns(columns, element, true) when is_map(element), do: [element | columns]
 | 
				
			||||||
 | 
					  def maybe_compose_columns(columns, _element_or_elements, false), do: columns
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,14 +4,15 @@
 | 
				
			|||||||
      <tr>
 | 
					      <tr>
 | 
				
			||||||
        <%= for %{key: key, label: label} = column <- @columns do %>
 | 
					        <%= for %{key: key, label: label} = column <- @columns do %>
 | 
				
			||||||
          <%= if column |> Map.get(:sortable, true) do %>
 | 
					          <%= if column |> Map.get(:sortable, true) do %>
 | 
				
			||||||
            <th class={"p-2 #{column[:class]}"}>
 | 
					            <th class={["p-2", column[:class]]}>
 | 
				
			||||||
              <span
 | 
					              <span
 | 
				
			||||||
                class="cursor-pointer flex justify-center items-center space-x-2"
 | 
					                class="cursor-pointer flex justify-center items-center space-x-2"
 | 
				
			||||||
                phx-click="sort_by"
 | 
					                phx-click="sort_by"
 | 
				
			||||||
                phx-value-sort-key={key}
 | 
					                phx-value-sort-key={key}
 | 
				
			||||||
                phx-target={@myself}
 | 
					                phx-target={@myself}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <span><%= label %></span>
 | 
					                <i class="w-0 float-right fas fa-sm fa-chevron-up opacity-0"></i>
 | 
				
			||||||
 | 
					                <span class={if @last_sort_key == key, do: "underline"}><%= label %></span>
 | 
				
			||||||
                <%= if @last_sort_key == key do %>
 | 
					                <%= if @last_sort_key == key do %>
 | 
				
			||||||
                  <%= case @sort_mode do %>
 | 
					                  <%= case @sort_mode do %>
 | 
				
			||||||
                    <% :asc -> %>
 | 
					                    <% :asc -> %>
 | 
				
			||||||
@@ -25,7 +26,7 @@
 | 
				
			|||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
            </th>
 | 
					            </th>
 | 
				
			||||||
          <% else %>
 | 
					          <% else %>
 | 
				
			||||||
            <th class={"p-2 #{column[:class]}"}>
 | 
					            <th class={["p-2 cursor-not-allowed", column[:class]]}>
 | 
				
			||||||
              <%= label %>
 | 
					              <%= label %>
 | 
				
			||||||
            </th>
 | 
					            </th>
 | 
				
			||||||
          <% end %>
 | 
					          <% end %>
 | 
				
			||||||
@@ -33,10 +34,11 @@
 | 
				
			|||||||
      </tr>
 | 
					      </tr>
 | 
				
			||||||
    </thead>
 | 
					    </thead>
 | 
				
			||||||
    <tbody>
 | 
					    <tbody>
 | 
				
			||||||
      <%= for {values, i} <- @rows |> Enum.with_index() do %>
 | 
					      <tr
 | 
				
			||||||
        <tr class={if i |> Integer.is_even(), do: @row_class, else: @alternate_row_class}>
 | 
					        :for={{values, i} <- @rows |> Enum.with_index()}
 | 
				
			||||||
          <%= for %{key: key} = value <- @columns do %>
 | 
					        class={if i |> Integer.is_even(), do: @row_class, else: @alternate_row_class}
 | 
				
			||||||
            <td class={"p-2 #{value[:class]}"}>
 | 
					      >
 | 
				
			||||||
 | 
					        <td :for={%{key: key} = value <- @columns} class={["p-2", value[:class]]}>
 | 
				
			||||||
          <%= case values |> Map.get(key) do %>
 | 
					          <%= case values |> Map.get(key) do %>
 | 
				
			||||||
            <% {_custom_sort_value, value} -> %>
 | 
					            <% {_custom_sort_value, value} -> %>
 | 
				
			||||||
              <%= value %>
 | 
					              <%= value %>
 | 
				
			||||||
@@ -44,9 +46,7 @@
 | 
				
			|||||||
              <%= value %>
 | 
					              <%= value %>
 | 
				
			||||||
          <% end %>
 | 
					          <% end %>
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
          <% end %>
 | 
					 | 
				
			||||||
      </tr>
 | 
					      </tr>
 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    </tbody>
 | 
					    </tbody>
 | 
				
			||||||
  </table>
 | 
					  </table>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,134 +0,0 @@
 | 
				
			|||||||
defmodule MemexWeb.Components.Topbar do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Component that renders a topbar with user functions/links
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  use MemexWeb, :component
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  alias Memex.Accounts
 | 
					 | 
				
			||||||
  alias MemexWeb.HomeLive
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def topbar(assigns) do
 | 
					 | 
				
			||||||
    assigns =
 | 
					 | 
				
			||||||
      %{results: [], title_content: nil, flash: nil, current_user: nil} |> Map.merge(assigns)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ~H"""
 | 
					 | 
				
			||||||
    <nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-900 text-primary-400">
 | 
					 | 
				
			||||||
      <div class="flex flex-col sm:flex-row justify-between items-center">
 | 
					 | 
				
			||||||
        <div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
 | 
					 | 
				
			||||||
          <.link
 | 
					 | 
				
			||||||
            navigate={Routes.live_path(Endpoint, HomeLive)}
 | 
					 | 
				
			||||||
            class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <%= gettext("memEx") %>
 | 
					 | 
				
			||||||
          </.link>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <%= if @title_content do %>
 | 
					 | 
				
			||||||
            <span class="mx-2 my-1">
 | 
					 | 
				
			||||||
              |
 | 
					 | 
				
			||||||
            </span>
 | 
					 | 
				
			||||||
            <%= @title_content %>
 | 
					 | 
				
			||||||
          <% end %>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <hr class="mb-2 sm:hidden hr-light" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <ul class="flex flex-row flex-wrap justify-center items-center
 | 
					 | 
				
			||||||
          text-lg text-primary-400 text-ellipsis">
 | 
					 | 
				
			||||||
          <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
            <.link
 | 
					 | 
				
			||||||
              navigate={Routes.note_index_path(Endpoint, :index)}
 | 
					 | 
				
			||||||
              class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <%= gettext("notes") %>
 | 
					 | 
				
			||||||
            </.link>
 | 
					 | 
				
			||||||
          </li>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
            <.link
 | 
					 | 
				
			||||||
              navigate={Routes.context_index_path(Endpoint, :index)}
 | 
					 | 
				
			||||||
              class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <%= gettext("contexts") %>
 | 
					 | 
				
			||||||
            </.link>
 | 
					 | 
				
			||||||
          </li>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
            <.link
 | 
					 | 
				
			||||||
              navigate={Routes.pipeline_index_path(Endpoint, :index)}
 | 
					 | 
				
			||||||
              class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <%= gettext("pipelines") %>
 | 
					 | 
				
			||||||
            </.link>
 | 
					 | 
				
			||||||
          </li>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <li class="mx-2 my-1 border-left border border-primary-700"></li>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <%= if @current_user do %>
 | 
					 | 
				
			||||||
            <%= if @current_user |> Accounts.is_already_admin?() do %>
 | 
					 | 
				
			||||||
              <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
                <.link
 | 
					 | 
				
			||||||
                  navigate={Routes.invite_index_path(Endpoint, :index)}
 | 
					 | 
				
			||||||
                  class="text-primary-400 text-primary-400 hover:underline"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <%= gettext("invites") %>
 | 
					 | 
				
			||||||
                </.link>
 | 
					 | 
				
			||||||
              </li>
 | 
					 | 
				
			||||||
            <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
              <.link
 | 
					 | 
				
			||||||
                navigate={Routes.user_settings_path(Endpoint, :edit)}
 | 
					 | 
				
			||||||
                class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <%= @current_user.email %>
 | 
					 | 
				
			||||||
              </.link>
 | 
					 | 
				
			||||||
            </li>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
              <.link
 | 
					 | 
				
			||||||
                href={Routes.user_session_path(Endpoint, :delete)}
 | 
					 | 
				
			||||||
                method="delete"
 | 
					 | 
				
			||||||
                data-confirm={dgettext("prompts", "are you sure you want to log out?")}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <i class="fas fa-sign-out-alt"></i>
 | 
					 | 
				
			||||||
              </.link>
 | 
					 | 
				
			||||||
            </li>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <%= if @current_user.role == :admin and function_exported?(Routes, :live_dashboard_path, 2) do %>
 | 
					 | 
				
			||||||
              <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
                <.link
 | 
					 | 
				
			||||||
                  navigate={Routes.live_dashboard_path(Endpoint, :home)}
 | 
					 | 
				
			||||||
                  class="text-primary-400 text-primary-400 hover:underline"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <i class="fas fa-gauge"></i>
 | 
					 | 
				
			||||||
                </.link>
 | 
					 | 
				
			||||||
              </li>
 | 
					 | 
				
			||||||
            <% end %>
 | 
					 | 
				
			||||||
          <% else %>
 | 
					 | 
				
			||||||
            <%= if Accounts.allow_registration?() do %>
 | 
					 | 
				
			||||||
              <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
                <.link
 | 
					 | 
				
			||||||
                  navigate={Routes.user_registration_path(Endpoint, :new)}
 | 
					 | 
				
			||||||
                  class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <%= dgettext("actions", "register") %>
 | 
					 | 
				
			||||||
                </.link>
 | 
					 | 
				
			||||||
              </li>
 | 
					 | 
				
			||||||
            <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <li class="mx-2 my-1">
 | 
					 | 
				
			||||||
              <.link
 | 
					 | 
				
			||||||
                navigate={Routes.user_session_path(Endpoint, :new)}
 | 
					 | 
				
			||||||
                class="text-primary-400 text-primary-400 hover:underline truncate"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <%= dgettext("actions", "log in") %>
 | 
					 | 
				
			||||||
              </.link>
 | 
					 | 
				
			||||||
            </li>
 | 
					 | 
				
			||||||
          <% end %>
 | 
					 | 
				
			||||||
        </ul>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </nav>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@@ -1,46 +0,0 @@
 | 
				
			|||||||
defmodule MemexWeb.Components.UserCard do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Display card for a user
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  use MemexWeb, :component
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def user_card(assigns) do
 | 
					 | 
				
			||||||
    ~H"""
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      id={"user-#{@user.id}"}
 | 
					 | 
				
			||||||
      class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
 | 
					 | 
				
			||||||
          border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
					 | 
				
			||||||
          transition-all duration-300 ease-in-out"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <h1 class="px-4 py-2 rounded-lg title text-xl break-all">
 | 
					 | 
				
			||||||
        <%= @user.email %>
 | 
					 | 
				
			||||||
      </h1>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
					 | 
				
			||||||
        <p>
 | 
					 | 
				
			||||||
          <%= if @user.confirmed_at |> is_nil() do %>
 | 
					 | 
				
			||||||
            <%= gettext("email unconfirmed") %>
 | 
					 | 
				
			||||||
          <% else %>
 | 
					 | 
				
			||||||
            <p>
 | 
					 | 
				
			||||||
              <%= gettext("user confirmed on") %>
 | 
					 | 
				
			||||||
              <%= @user.confirmed_at |> display_datetime() %>
 | 
					 | 
				
			||||||
            </p>
 | 
					 | 
				
			||||||
          <% end %>
 | 
					 | 
				
			||||||
        </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <p>
 | 
					 | 
				
			||||||
          <%= gettext("user registered on") %>
 | 
					 | 
				
			||||||
          <%= @user.inserted_at |> display_datetime() %>
 | 
					 | 
				
			||||||
        </p>
 | 
					 | 
				
			||||||
      </h3>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <%= if @inner_block do %>
 | 
					 | 
				
			||||||
        <div class="px-4 py-2 flex space-x-4 justify-center items-center">
 | 
					 | 
				
			||||||
          <%= render_slot(@inner_block) %>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      <% end %>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
@@ -6,7 +6,8 @@ defmodule MemexWeb.EmailController do
 | 
				
			|||||||
  use MemexWeb, :controller
 | 
					  use MemexWeb, :controller
 | 
				
			||||||
  alias Memex.Accounts.User
 | 
					  alias Memex.Accounts.User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  plug :put_layout, {MemexWeb.LayoutView, :email}
 | 
					  plug :put_root_layout, html: {MemexWeb.Layouts, :email_html}
 | 
				
			||||||
 | 
					  plug :put_layout, false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @sample_assigns %{
 | 
					  @sample_assigns %{
 | 
				
			||||||
    email: %{subject: "Example subject"},
 | 
					    email: %{subject: "Example subject"},
 | 
				
			||||||
@@ -18,6 +19,6 @@ defmodule MemexWeb.EmailController do
 | 
				
			|||||||
  Debug route used to preview emails
 | 
					  Debug route used to preview emails
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
  def preview(conn, %{"id" => template}) do
 | 
					  def preview(conn, %{"id" => template}) do
 | 
				
			||||||
    render(conn, "#{template |> to_string()}.html", @sample_assigns)
 | 
					    render(conn, String.to_existing_atom(template), @sample_assigns)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								lib/memex_web/controllers/email_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								lib/memex_web/controllers/email_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.EmailHTML do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  Renders email templates
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "email_html/*.html", suffix: "_html"
 | 
				
			||||||
 | 
					  embed_templates "email_html/*.txt", suffix: "_text"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -15,7 +15,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <br />
 | 
					  <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
 | 
					  <a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <br />
 | 
					  <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,4 +9,4 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<%= dgettext("emails",
 | 
					<%= dgettext("emails",
 | 
				
			||||||
  "If you didn't create an account at %{url}, please ignore this.",
 | 
					  "If you didn't create an account at %{url}, please ignore this.",
 | 
				
			||||||
  url: Routes.live_url(Endpoint, HomeLive)) %>
 | 
					  url: ~p"/") %>
 | 
				
			||||||
@@ -9,7 +9,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <br />
 | 
					  <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
 | 
					  <a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <br />
 | 
					  <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7,4 +7,4 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<%= dgettext("emails",
 | 
					<%= dgettext("emails",
 | 
				
			||||||
  "If you didn't request this change from %{url}, please ignore this.",
 | 
					  "If you didn't request this change from %{url}, please ignore this.",
 | 
				
			||||||
  url: Routes.live_url(Endpoint, HomeLive)) %>
 | 
					  url: ~p"/") %>
 | 
				
			||||||
@@ -9,7 +9,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <br />
 | 
					  <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
 | 
					  <a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <br />
 | 
					  <br />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7,4 +7,4 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<%= dgettext("emails",
 | 
					<%= dgettext("emails",
 | 
				
			||||||
  "If you didn't request this change from %{url}, please ignore this.",
 | 
					  "If you didn't request this change from %{url}, please ignore this.",
 | 
				
			||||||
  url: Routes.live_url(Endpoint, HomeLive)) %>
 | 
					  url: ~p"/") %>
 | 
				
			||||||
							
								
								
									
										16
									
								
								lib/memex_web/controllers/error_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/memex_web/controllers/error_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.ErrorHTML do
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "error_html/*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def render(template, _assigns) do
 | 
				
			||||||
 | 
					    error_string =
 | 
				
			||||||
 | 
					      case template do
 | 
				
			||||||
 | 
					        "404.html" -> dgettext("errors", "not found")
 | 
				
			||||||
 | 
					        "401.html" -> dgettext("errors", "unauthorized")
 | 
				
			||||||
 | 
					        _other_path -> dgettext("errors", "internal server error")
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    error(%{error_string: error_string})
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -5,15 +5,15 @@
 | 
				
			|||||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
					    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||||
    <title>
 | 
					    <title>
 | 
				
			||||||
      <%= dgettext("errors", "Error") %> | memEx
 | 
					      <%= dgettext("errors", "Error") %> | <%= gettext("memEx") %>
 | 
				
			||||||
    </title>
 | 
					    </title>
 | 
				
			||||||
    <link rel="stylesheet" href="/css/app.css" />
 | 
					    <link rel="stylesheet" href="/css/app.css" />
 | 
				
			||||||
    <script defer type="text/javascript" src="/js/app.js">
 | 
					    <script defer type="text/javascript" src="/js/app.js">
 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body class="m-0 p-0 w-full h-full bg-primary-800 text-primary-400 subpixel-antialiased">
 | 
					  <body class="pb-8 m-0 p-0 w-full h-full bg-primary-800 text-primary-400 subpixel-antialiased">
 | 
				
			||||||
    <header>
 | 
					    <header>
 | 
				
			||||||
      <.topbar current_user={assigns[:current_user]}></.topbar>
 | 
					      <.topbar current_user={assigns[:current_user]} />
 | 
				
			||||||
    </header>
 | 
					    </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="pb-8 w-full flex flex-col justify-center items-center text-center">
 | 
					    <div class="pb-8 w-full flex flex-col justify-center items-center text-center">
 | 
				
			||||||
@@ -24,9 +24,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <hr class="w-full hr" />
 | 
					        <hr class="w-full hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <a href={Routes.live_path(Endpoint, HomeLive)} class="link title text-primary-400 text-lg">
 | 
					        <.link href={~p"/"} class="link title text-primary-400 text-lg">
 | 
				
			||||||
          <%= dgettext("errors", "go back home") %>
 | 
					          <%= dgettext("errors", "go back home") %>
 | 
				
			||||||
        </a>
 | 
					        </.link>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
							
								
								
									
										14
									
								
								lib/memex_web/controllers/error_json.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								lib/memex_web/controllers/error_json.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.ErrorJSON do
 | 
				
			||||||
 | 
					  import MemexWeb.Gettext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def render(template, _assigns) do
 | 
				
			||||||
 | 
					    error_string =
 | 
				
			||||||
 | 
					      case template do
 | 
				
			||||||
 | 
					        "404.json" -> dgettext("errors", "not found")
 | 
				
			||||||
 | 
					        "401.json" -> dgettext("errors", "unauthorized")
 | 
				
			||||||
 | 
					        _other_path -> dgettext("errors", "internal server error")
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    %{errors: %{detail: error_string}}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										17
									
								
								lib/memex_web/controllers/export_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/memex_web/controllers/export_controller.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.ExportController do
 | 
				
			||||||
 | 
					  use MemexWeb, :controller
 | 
				
			||||||
 | 
					  alias Memex.{Contexts, Notes, Pipelines, Pipelines.Steps}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def export(%{assigns: %{current_user: current_user}} = conn, %{"mode" => "json"}) do
 | 
				
			||||||
 | 
					    pipelines =
 | 
				
			||||||
 | 
					      Pipelines.list_pipelines(current_user)
 | 
				
			||||||
 | 
					      |> Enum.map(fn pipeline -> Steps.preload_steps(pipeline, current_user) end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    json(conn, %{
 | 
				
			||||||
 | 
					      user: current_user,
 | 
				
			||||||
 | 
					      notes: Notes.list_notes(current_user),
 | 
				
			||||||
 | 
					      contexts: Contexts.list_contexts(current_user),
 | 
				
			||||||
 | 
					      pipelines: pipelines
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -1,11 +0,0 @@
 | 
				
			|||||||
defmodule MemexWeb.HomeController do
 | 
					 | 
				
			||||||
  @moduledoc """
 | 
					 | 
				
			||||||
  Controller for home page
 | 
					 | 
				
			||||||
  """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  use MemexWeb, :controller
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def index(conn, _params) do
 | 
					 | 
				
			||||||
    render(conn, "index.html")
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
							
								
								
									
										5
									
								
								lib/memex_web/controllers/home_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/memex_web/controllers/home_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.HomeHTML do
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "home_html/*"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -3,12 +3,11 @@ defmodule MemexWeb.UserAuth do
 | 
				
			|||||||
  Functions for user session and authentication
 | 
					  Functions for user session and authentication
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  use MemexWeb, :verified_routes
 | 
				
			||||||
  import Plug.Conn
 | 
					  import Plug.Conn
 | 
				
			||||||
  import Phoenix.Controller
 | 
					  import Phoenix.Controller
 | 
				
			||||||
  import MemexWeb.Gettext
 | 
					  import MemexWeb.Gettext
 | 
				
			||||||
  alias Memex.{Accounts, Accounts.User}
 | 
					  alias Memex.{Accounts, Accounts.User}
 | 
				
			||||||
  alias MemexWeb.HomeLive
 | 
					 | 
				
			||||||
  alias MemexWeb.Router.Helpers, as: Routes
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Make the remember me cookie valid for 60 days.
 | 
					  # Make the remember me cookie valid for 60 days.
 | 
				
			||||||
  # If you want bump or reduce this value, also change
 | 
					  # If you want bump or reduce this value, also change
 | 
				
			||||||
@@ -39,7 +38,7 @@ defmodule MemexWeb.UserAuth do
 | 
				
			|||||||
      dgettext("errors", "You must confirm your account and log in to access this page.")
 | 
					      dgettext("errors", "You must confirm your account and log in to access this page.")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    |> maybe_store_return_to()
 | 
					    |> maybe_store_return_to()
 | 
				
			||||||
    |> redirect(to: Routes.user_session_path(conn, :new))
 | 
					    |> redirect(to: ~p"/users/log_in")
 | 
				
			||||||
    |> halt()
 | 
					    |> halt()
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,8 +48,7 @@ defmodule MemexWeb.UserAuth do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    conn
 | 
					    conn
 | 
				
			||||||
    |> renew_session()
 | 
					    |> renew_session()
 | 
				
			||||||
    |> put_session(:user_token, token)
 | 
					    |> put_token_in_session(token)
 | 
				
			||||||
    |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
 | 
					 | 
				
			||||||
    |> maybe_write_remember_me_cookie(token, params)
 | 
					    |> maybe_write_remember_me_cookie(token, params)
 | 
				
			||||||
    |> redirect(to: user_return_to || signed_in_path(conn))
 | 
					    |> redirect(to: user_return_to || signed_in_path(conn))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -96,7 +94,7 @@ defmodule MemexWeb.UserAuth do
 | 
				
			|||||||
  """
 | 
					  """
 | 
				
			||||||
  def log_out_user(conn) do
 | 
					  def log_out_user(conn) do
 | 
				
			||||||
    user_token = get_session(conn, :user_token)
 | 
					    user_token = get_session(conn, :user_token)
 | 
				
			||||||
    user_token && Accounts.delete_session_token(user_token)
 | 
					    user_token && Accounts.delete_user_session_token(user_token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if live_socket_id = get_session(conn, :live_socket_id) do
 | 
					    if live_socket_id = get_session(conn, :live_socket_id) do
 | 
				
			||||||
      MemexWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
 | 
					      MemexWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
 | 
				
			||||||
@@ -105,7 +103,7 @@ defmodule MemexWeb.UserAuth do
 | 
				
			|||||||
    conn
 | 
					    conn
 | 
				
			||||||
    |> renew_session()
 | 
					    |> renew_session()
 | 
				
			||||||
    |> delete_resp_cookie(@remember_me_cookie)
 | 
					    |> delete_resp_cookie(@remember_me_cookie)
 | 
				
			||||||
    |> redirect(to: "/")
 | 
					    |> redirect(to: ~p"/")
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
@@ -119,19 +117,110 @@ defmodule MemexWeb.UserAuth do
 | 
				
			|||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp ensure_user_token(conn) do
 | 
					  defp ensure_user_token(conn) do
 | 
				
			||||||
    if user_token = get_session(conn, :user_token) do
 | 
					    if token = get_session(conn, :user_token) do
 | 
				
			||||||
      {user_token, conn}
 | 
					      {token, conn}
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      conn = fetch_cookies(conn, signed: [@remember_me_cookie])
 | 
					      conn = fetch_cookies(conn, signed: [@remember_me_cookie])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if user_token = conn.cookies[@remember_me_cookie] do
 | 
					      if token = conn.cookies[@remember_me_cookie] do
 | 
				
			||||||
        {user_token, put_session(conn, :user_token, user_token)}
 | 
					        {token, put_token_in_session(conn, token)}
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        {nil, conn}
 | 
					        {nil, conn}
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @doc """
 | 
				
			||||||
 | 
					  Handles mounting and authenticating the current_user in LiveViews.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## `on_mount` arguments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * `:mount_current_user` - Assigns current_user
 | 
				
			||||||
 | 
					      to socket assigns based on user_token, or nil if
 | 
				
			||||||
 | 
					      there's no user_token or no matching user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * `:ensure_authenticated` - Authenticates the user from the session,
 | 
				
			||||||
 | 
					      and assigns the current_user to socket assigns based
 | 
				
			||||||
 | 
					      on user_token.
 | 
				
			||||||
 | 
					      Redirects to login page if there's no logged user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
 | 
				
			||||||
 | 
					      Redirects to signed_in_path if there's a logged user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ## Examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
 | 
				
			||||||
 | 
					  the current_user:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      defmodule MemexWeb.PageLive do
 | 
				
			||||||
 | 
					        use MemexWeb, :live_view
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        on_mount {MemexWeb.UserAuth, :mount_current_user}
 | 
				
			||||||
 | 
					        ...
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Or use the `live_session` of your router to invoke the on_mount callback:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      live_session :authenticated, on_mount: [{MemexWeb.UserAuth, :ensure_authenticated}] do
 | 
				
			||||||
 | 
					        live "/profile", ProfileLive, :index
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  def on_mount(:mount_current_user, _params, session, socket) do
 | 
				
			||||||
 | 
					    {:cont, mount_current_user(session, socket)}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def on_mount(:ensure_authenticated, _params, session, socket) do
 | 
				
			||||||
 | 
					    socket = mount_current_user(session, socket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if socket.assigns.current_user do
 | 
				
			||||||
 | 
					      {:cont, socket}
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      error_flash = dgettext("errors", "You must log in to access this page.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      socket =
 | 
				
			||||||
 | 
					        socket
 | 
				
			||||||
 | 
					        |> Phoenix.LiveView.put_flash(:error, error_flash)
 | 
				
			||||||
 | 
					        |> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {:halt, socket}
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def on_mount(:ensure_admin, _params, session, socket) do
 | 
				
			||||||
 | 
					    socket = mount_current_user(session, socket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if socket.assigns.current_user && socket.assigns.current_user.role == :admin do
 | 
				
			||||||
 | 
					      {:cont, socket}
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      error_flash = dgettext("errors", "You must log in as an administrator to access this page.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      socket =
 | 
				
			||||||
 | 
					        socket
 | 
				
			||||||
 | 
					        |> Phoenix.LiveView.put_flash(:error, error_flash)
 | 
				
			||||||
 | 
					        |> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {:halt, socket}
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
 | 
				
			||||||
 | 
					    socket = mount_current_user(session, socket)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if socket.assigns.current_user do
 | 
				
			||||||
 | 
					      {:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      {:cont, socket}
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp mount_current_user(session, socket) do
 | 
				
			||||||
 | 
					    Phoenix.Component.assign_new(socket, :current_user, fn ->
 | 
				
			||||||
 | 
					      if user_token = session["user_token"] do
 | 
				
			||||||
 | 
					        Accounts.get_user_by_session_token(user_token)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @doc """
 | 
					  @doc """
 | 
				
			||||||
  Used for routes that require the user to not be authenticated.
 | 
					  Used for routes that require the user to not be authenticated.
 | 
				
			||||||
  """
 | 
					  """
 | 
				
			||||||
@@ -161,7 +250,7 @@ defmodule MemexWeb.UserAuth do
 | 
				
			|||||||
        dgettext("errors", "You must confirm your account and log in to access this page.")
 | 
					        dgettext("errors", "You must confirm your account and log in to access this page.")
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      |> maybe_store_return_to()
 | 
					      |> maybe_store_return_to()
 | 
				
			||||||
      |> redirect(to: Routes.user_session_path(conn, :new))
 | 
					      |> redirect(to: ~p"/users/log_in")
 | 
				
			||||||
      |> halt()
 | 
					      |> halt()
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
@@ -176,16 +265,34 @@ defmodule MemexWeb.UserAuth do
 | 
				
			|||||||
      conn
 | 
					      conn
 | 
				
			||||||
      |> put_flash(:error, dgettext("errors", "You are not authorized to view this page."))
 | 
					      |> put_flash(:error, dgettext("errors", "You are not authorized to view this page."))
 | 
				
			||||||
      |> maybe_store_return_to()
 | 
					      |> maybe_store_return_to()
 | 
				
			||||||
      |> redirect(to: Routes.live_path(conn, HomeLive))
 | 
					      |> redirect(to: ~p"/")
 | 
				
			||||||
      |> halt()
 | 
					      |> halt()
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, _opts) do
 | 
				
			||||||
 | 
					    default = Application.fetch_env!(:gettext, :default_locale)
 | 
				
			||||||
 | 
					    Gettext.put_locale(locale || default)
 | 
				
			||||||
 | 
					    conn |> put_session(:locale, locale || default)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def put_user_locale(conn, _opts) do
 | 
				
			||||||
 | 
					    default = Application.fetch_env!(:gettext, :default_locale)
 | 
				
			||||||
 | 
					    Gettext.put_locale(default)
 | 
				
			||||||
 | 
					    conn |> put_session(:locale, default)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp put_token_in_session(conn, token) do
 | 
				
			||||||
 | 
					    conn
 | 
				
			||||||
 | 
					    |> put_session(:user_token, token)
 | 
				
			||||||
 | 
					    |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp maybe_store_return_to(%{method: "GET"} = conn) do
 | 
					  defp maybe_store_return_to(%{method: "GET"} = conn) do
 | 
				
			||||||
    put_session(conn, :user_return_to, current_path(conn))
 | 
					    put_session(conn, :user_return_to, current_path(conn))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp maybe_store_return_to(conn), do: conn
 | 
					  defp maybe_store_return_to(conn), do: conn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp signed_in_path(_conn), do: "/"
 | 
					  defp signed_in_path(_conn), do: ~p"/"
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,14 +5,14 @@ defmodule MemexWeb.UserConfirmationController do
 | 
				
			|||||||
  alias Memex.Accounts
 | 
					  alias Memex.Accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new(conn, _params) do
 | 
					  def new(conn, _params) do
 | 
				
			||||||
    render(conn, "new.html", page_title: gettext("Confirm your account"))
 | 
					    render(conn, :new, page_title: gettext("confirm your account"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create(conn, %{"user" => %{"email" => email}}) do
 | 
					  def create(conn, %{"user" => %{"email" => email}}) do
 | 
				
			||||||
    if user = Accounts.get_user_by_email(email) do
 | 
					    if user = Accounts.get_user_by_email(email) do
 | 
				
			||||||
      Accounts.deliver_user_confirmation_instructions(
 | 
					      Accounts.deliver_user_confirmation_instructions(
 | 
				
			||||||
        user,
 | 
					        user,
 | 
				
			||||||
        &Routes.user_confirmation_url(conn, :confirm, &1)
 | 
					        fn token -> url(MemexWeb.Endpoint, ~p"/users/confirm/#{token}") end
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,11 +22,10 @@ defmodule MemexWeb.UserConfirmationController do
 | 
				
			|||||||
      :info,
 | 
					      :info,
 | 
				
			||||||
      dgettext(
 | 
					      dgettext(
 | 
				
			||||||
        "prompts",
 | 
					        "prompts",
 | 
				
			||||||
        "If your email is in our system and it has not been confirmed yet, " <>
 | 
					        "if your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
 | 
				
			||||||
          "you will receive an email with instructions shortly."
 | 
					 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    |> redirect(to: "/")
 | 
					    |> redirect(to: ~p"/")
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Do not log in the user after confirmation to avoid a
 | 
					  # Do not log in the user after confirmation to avoid a
 | 
				
			||||||
@@ -36,7 +35,7 @@ defmodule MemexWeb.UserConfirmationController do
 | 
				
			|||||||
      {:ok, %{email: email}} ->
 | 
					      {:ok, %{email: email}} ->
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
        |> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email))
 | 
					        |> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email))
 | 
				
			||||||
        |> redirect(to: "/")
 | 
					        |> redirect(to: ~p"/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      :error ->
 | 
					      :error ->
 | 
				
			||||||
        # If there is a current user and the account was already confirmed,
 | 
					        # If there is a current user and the account was already confirmed,
 | 
				
			||||||
@@ -45,15 +44,15 @@ defmodule MemexWeb.UserConfirmationController do
 | 
				
			|||||||
        # a warning message.
 | 
					        # a warning message.
 | 
				
			||||||
        case conn.assigns do
 | 
					        case conn.assigns do
 | 
				
			||||||
          %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
 | 
					          %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
 | 
				
			||||||
            redirect(conn, to: "/")
 | 
					            redirect(conn, to: ~p"/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          %{} ->
 | 
					          %{} ->
 | 
				
			||||||
            conn
 | 
					            conn
 | 
				
			||||||
            |> put_flash(
 | 
					            |> put_flash(
 | 
				
			||||||
              :error,
 | 
					              :error,
 | 
				
			||||||
              dgettext("errors", "User confirmation link is invalid or it has expired.")
 | 
					              dgettext("errors", "user confirmation link is invalid or it has expired.")
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            |> redirect(to: "/")
 | 
					            |> redirect(to: ~p"/")
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								lib/memex_web/controllers/user_confirmation_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/memex_web/controllers/user_confirmation_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.UserConfirmationHTML do
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					  alias Memex.Accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "user_confirmation_html/*"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -1,15 +1,16 @@
 | 
				
			|||||||
<div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
					<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
				
			||||||
  <h1 class="title text-primary-400 text-xl">
 | 
					  <h1 class="title text-primary-400 text-xl">
 | 
				
			||||||
    <%= dgettext("actions", "Resend confirmation instructions") %>
 | 
					    <%= dgettext("actions", "Resend confirmation instructions") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    :let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={:user}
 | 
					    for={%{}}
 | 
				
			||||||
    action={Routes.user_confirmation_path(@conn, :create)}
 | 
					    as={:user}
 | 
				
			||||||
 | 
					    action={~p"/users/confirm"}
 | 
				
			||||||
    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <%= label(f, :email, class: "title text-lg text-primary-400") %>
 | 
					    <%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-400") %>
 | 
				
			||||||
    <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
 | 
					    <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= submit(dgettext("actions", "Resend confirmation instructions"),
 | 
					    <%= submit(dgettext("actions", "Resend confirmation instructions"),
 | 
				
			||||||
@@ -20,12 +21,10 @@
 | 
				
			|||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if Accounts.allow_registration?() do %>
 | 
					    <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
 | 
				
			||||||
      <.link href={Routes.user_registration_path(@conn, :new)} class="btn btn-primary">
 | 
					 | 
				
			||||||
      <%= dgettext("actions", "register") %>
 | 
					      <%= dgettext("actions", "register") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
    <% end %>
 | 
					    <.link href={~p"/users/log_in"} class="btn btn-primary">
 | 
				
			||||||
    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
					 | 
				
			||||||
      <%= dgettext("actions", "log in") %>
 | 
					      <%= dgettext("actions", "log in") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -1,18 +1,15 @@
 | 
				
			|||||||
defmodule MemexWeb.UserRegistrationController do
 | 
					defmodule MemexWeb.UserRegistrationController do
 | 
				
			||||||
  use MemexWeb, :controller
 | 
					  use MemexWeb, :controller
 | 
				
			||||||
  import MemexWeb.Gettext
 | 
					  import MemexWeb.Gettext
 | 
				
			||||||
  alias Memex.{Accounts, Invites}
 | 
					  alias Memex.{Accounts, Accounts.Invites}
 | 
				
			||||||
  alias MemexWeb.HomeLive
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new(conn, %{"invite" => invite_token}) do
 | 
					  def new(conn, %{"invite" => invite_token}) do
 | 
				
			||||||
    invite = Invites.get_invite_by_token(invite_token)
 | 
					    if Invites.valid_invite_token?(invite_token) do
 | 
				
			||||||
 | 
					      conn |> render_new(invite_token)
 | 
				
			||||||
    if invite do
 | 
					 | 
				
			||||||
      conn |> render_new(invite)
 | 
					 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      conn
 | 
					      conn
 | 
				
			||||||
      |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
 | 
					      |> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
 | 
				
			||||||
      |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
					      |> redirect(to: ~p"/")
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,29 +18,27 @@ defmodule MemexWeb.UserRegistrationController do
 | 
				
			|||||||
      conn |> render_new()
 | 
					      conn |> render_new()
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      conn
 | 
					      conn
 | 
				
			||||||
      |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
 | 
					      |> put_flash(:error, dgettext("errors", "sorry, public registration is disabled"))
 | 
				
			||||||
      |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
					      |> redirect(to: ~p"/")
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # renders new user registration page
 | 
					  # renders new user registration page
 | 
				
			||||||
  defp render_new(conn, invite \\ nil) do
 | 
					  defp render_new(conn, invite_token \\ nil) do
 | 
				
			||||||
    render(conn, "new.html",
 | 
					    render(conn, :new,
 | 
				
			||||||
      changeset: Accounts.change_user_registration(),
 | 
					      changeset: Accounts.change_user_registration(),
 | 
				
			||||||
      invite: invite,
 | 
					      invite_token: invite_token,
 | 
				
			||||||
      page_title: gettext("register")
 | 
					      page_title: gettext("register")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do
 | 
					  def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do
 | 
				
			||||||
    invite = Invites.get_invite_by_token(invite_token)
 | 
					    if Invites.valid_invite_token?(invite_token) do
 | 
				
			||||||
 | 
					      conn |> create_user(attrs, invite_token)
 | 
				
			||||||
    if invite do
 | 
					 | 
				
			||||||
      conn |> create_user(attrs, invite)
 | 
					 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      conn
 | 
					      conn
 | 
				
			||||||
      |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
 | 
					      |> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
 | 
				
			||||||
      |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
					      |> redirect(to: ~p"/")
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,29 +47,30 @@ defmodule MemexWeb.UserRegistrationController do
 | 
				
			|||||||
      conn |> create_user(attrs)
 | 
					      conn |> create_user(attrs)
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      conn
 | 
					      conn
 | 
				
			||||||
      |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
 | 
					      |> put_flash(:error, dgettext("errors", "sorry, public registration is disabled"))
 | 
				
			||||||
      |> redirect(to: Routes.live_path(Endpoint, HomeLive))
 | 
					      |> redirect(to: ~p"/")
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  defp create_user(conn, %{"user" => user_params}, invite \\ nil) do
 | 
					  defp create_user(conn, %{"user" => user_params}, invite_token \\ nil) do
 | 
				
			||||||
    case Accounts.register_user(user_params) do
 | 
					    case Accounts.register_user(user_params, invite_token) do
 | 
				
			||||||
      {:ok, user} ->
 | 
					      {:ok, user} ->
 | 
				
			||||||
        unless invite |> is_nil() do
 | 
					 | 
				
			||||||
          invite |> Invites.use_invite!()
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Accounts.deliver_user_confirmation_instructions(
 | 
					        Accounts.deliver_user_confirmation_instructions(
 | 
				
			||||||
          user,
 | 
					          user,
 | 
				
			||||||
          &Routes.user_confirmation_url(conn, :confirm, &1)
 | 
					          fn token -> url(MemexWeb.Endpoint, ~p"/users/confirm/#{token}") end
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
        |> put_flash(:info, dgettext("prompts", "Please check your email to verify your account"))
 | 
					        |> put_flash(:info, dgettext("prompts", "please check your email to verify your account"))
 | 
				
			||||||
        |> redirect(to: Routes.user_session_path(Endpoint, :new))
 | 
					        |> redirect(to: ~p"/users/log_in")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {:error, :invalid_token} ->
 | 
				
			||||||
 | 
					        conn
 | 
				
			||||||
 | 
					        |> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
 | 
				
			||||||
 | 
					        |> redirect(to: ~p"/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {:error, %Ecto.Changeset{} = changeset} ->
 | 
					      {:error, %Ecto.Changeset{} = changeset} ->
 | 
				
			||||||
        conn |> render("new.html", changeset: changeset, invite: invite)
 | 
					        conn |> render("new.html", changeset: changeset, invite_token: invite_token)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								lib/memex_web/controllers/user_registration_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/memex_web/controllers/user_registration_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.UserRegistrationHTML do
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "user_registration_html/*"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
<div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
					<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
				
			||||||
  <h1 class="title text-primary-400 text-xl">
 | 
					  <h1 class="title text-primary-400 text-xl">
 | 
				
			||||||
    <%= dgettext("actions", "register") %>
 | 
					    <%= dgettext("actions", "register") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
@@ -6,30 +6,26 @@
 | 
				
			|||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    :let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    action={Routes.user_registration_path(@conn, :create)}
 | 
					    action={~p"/users/register"}
 | 
				
			||||||
    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <%= if @changeset.action && not @changeset.valid? do %>
 | 
					    <p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
 | 
				
			||||||
      <div class="alert alert-danger col-span-3">
 | 
					      <%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
 | 
				
			||||||
        <p>
 | 
					 | 
				
			||||||
          <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
 | 
					 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
      </div>
 | 
					
 | 
				
			||||||
 | 
					    <%= if @invite_token do %>
 | 
				
			||||||
 | 
					      <%= hidden_input(f, :invite_token, value: @invite_token) %>
 | 
				
			||||||
    <% end %>
 | 
					    <% end %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= if @invite do %>
 | 
					    <%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
 | 
				
			||||||
      <%= hidden_input(f, :invite_token, value: @invite.token) %>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <%= label(f, :email, class: "title text-lg text-primary-400") %>
 | 
					 | 
				
			||||||
    <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
 | 
					    <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
 | 
				
			||||||
    <%= error_tag(f, :email, "col-span-3") %>
 | 
					    <%= error_tag(f, :email, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :password, class: "title text-lg text-primary-400") %>
 | 
					    <%= label(f, :password, gettext("password"), class: "title text-lg text-primary-400") %>
 | 
				
			||||||
    <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
 | 
					    <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
 | 
				
			||||||
    <%= error_tag(f, :password, "col-span-3") %>
 | 
					    <%= error_tag(f, :password, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :locale, gettext("Language"), class: "title text-lg text-primary-400") %>
 | 
					    <%= label(f, :locale, gettext("language"), class: "title text-lg text-primary-400") %>
 | 
				
			||||||
    <%= select(
 | 
					    <%= select(
 | 
				
			||||||
      f,
 | 
					      f,
 | 
				
			||||||
      :locale,
 | 
					      :locale,
 | 
				
			||||||
@@ -44,10 +40,10 @@
 | 
				
			|||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
					    <.link href={~p"/users/log_in"} class="btn btn-primary">
 | 
				
			||||||
      <%= dgettext("actions", "log in") %>
 | 
					      <%= dgettext("actions", "log in") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
    <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
 | 
					    <.link href={~p"/users/reset_password"} class="btn btn-primary">
 | 
				
			||||||
      <%= dgettext("actions", "forgot your password?") %>
 | 
					      <%= dgettext("actions", "forgot your password?") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -6,14 +6,14 @@ defmodule MemexWeb.UserResetPasswordController do
 | 
				
			|||||||
  plug :get_user_by_reset_password_token when action in [:edit, :update]
 | 
					  plug :get_user_by_reset_password_token when action in [:edit, :update]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new(conn, _params) do
 | 
					  def new(conn, _params) do
 | 
				
			||||||
    render(conn, "new.html", page_title: gettext("forgot your password?"))
 | 
					    render(conn, :new, page_title: gettext("forgot your password?"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create(conn, %{"user" => %{"email" => email}}) do
 | 
					  def create(conn, %{"user" => %{"email" => email}}) do
 | 
				
			||||||
    if user = Accounts.get_user_by_email(email) do
 | 
					    if user = Accounts.get_user_by_email(email) do
 | 
				
			||||||
      Accounts.deliver_user_reset_password_instructions(
 | 
					      Accounts.deliver_user_reset_password_instructions(
 | 
				
			||||||
        user,
 | 
					        user,
 | 
				
			||||||
        &Routes.user_reset_password_url(conn, :edit, &1)
 | 
					        fn token -> url(MemexWeb.Endpoint, ~p"/users/reset_password/#{token}") end
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,17 +23,16 @@ defmodule MemexWeb.UserResetPasswordController do
 | 
				
			|||||||
      :info,
 | 
					      :info,
 | 
				
			||||||
      dgettext(
 | 
					      dgettext(
 | 
				
			||||||
        "prompts",
 | 
					        "prompts",
 | 
				
			||||||
        "If your email is in our system, you will receive instructions to " <>
 | 
					        "if your email is in our system, you will receive instructions to reset your password shortly."
 | 
				
			||||||
          "reset your password shortly."
 | 
					 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    |> redirect(to: "/")
 | 
					    |> redirect(to: ~p"/")
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def edit(conn, _params) do
 | 
					  def edit(conn, _params) do
 | 
				
			||||||
    render(conn, "edit.html",
 | 
					    render(conn, :edit,
 | 
				
			||||||
      changeset: Accounts.change_user_password(conn.assigns.user),
 | 
					      changeset: Accounts.change_user_password(conn.assigns.user),
 | 
				
			||||||
      page_title: gettext("Reset your password")
 | 
					      page_title: gettext("reset your password")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,11 +42,11 @@ defmodule MemexWeb.UserResetPasswordController do
 | 
				
			|||||||
    case Accounts.reset_user_password(conn.assigns.user, user_params) do
 | 
					    case Accounts.reset_user_password(conn.assigns.user, user_params) do
 | 
				
			||||||
      {:ok, _} ->
 | 
					      {:ok, _} ->
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
        |> put_flash(:info, dgettext("prompts", "Password reset successfully."))
 | 
					        |> put_flash(:info, dgettext("prompts", "password reset successfully."))
 | 
				
			||||||
        |> redirect(to: Routes.user_session_path(conn, :new))
 | 
					        |> redirect(to: ~p"/users/log_in")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {:error, changeset} ->
 | 
					      {:error, changeset} ->
 | 
				
			||||||
        render(conn, "edit.html", changeset: changeset)
 | 
					        render(conn, :edit, changeset: changeset)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -60,9 +59,9 @@ defmodule MemexWeb.UserResetPasswordController do
 | 
				
			|||||||
      conn
 | 
					      conn
 | 
				
			||||||
      |> put_flash(
 | 
					      |> put_flash(
 | 
				
			||||||
        :error,
 | 
					        :error,
 | 
				
			||||||
        dgettext("errors", "Reset password link is invalid or it has expired.")
 | 
					        dgettext("errors", "reset password link is invalid or it has expired.")
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      |> redirect(to: "/")
 | 
					      |> redirect(to: ~p"/")
 | 
				
			||||||
      |> halt()
 | 
					      |> halt()
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								lib/memex_web/controllers/user_reset_password_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/memex_web/controllers/user_reset_password_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.UserResetPasswordHTML do
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					  alias Memex.Accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "user_reset_password_html/*"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
<div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
					<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
				
			||||||
  <h1 class="title text-primary-400 text-xl">
 | 
					  <h1 class="title text-primary-400 text-xl">
 | 
				
			||||||
    <%= dgettext("actions", "Reset password") %>
 | 
					    <%= dgettext("actions", "Reset password") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
@@ -6,22 +6,18 @@
 | 
				
			|||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    :let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@changeset}
 | 
					    for={@changeset}
 | 
				
			||||||
    action={Routes.user_reset_password_path(@conn, :update, @token)}
 | 
					    action={~p"/users/reset_password/#{@token}"}
 | 
				
			||||||
    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <%= if @changeset.action && not @changeset.valid? do %>
 | 
					    <p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
 | 
				
			||||||
      <div class="alert alert-danger col-span-3">
 | 
					      <%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
 | 
				
			||||||
        <p>
 | 
					 | 
				
			||||||
          <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
 | 
					 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :password, "new password", class: "title text-lg text-primary-400") %>
 | 
					    <%= label(f, :password, gettext("new password"), class: "title text-lg text-primary-400") %>
 | 
				
			||||||
    <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
 | 
					    <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
 | 
				
			||||||
    <%= error_tag(f, :password, "col-span-3") %>
 | 
					    <%= error_tag(f, :password, "col-span-3") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :password_confirmation, "Confirm new password",
 | 
					    <%= label(f, :password_confirmation, gettext("confirm new password"),
 | 
				
			||||||
      class: "title text-lg text-primary-400"
 | 
					      class: "title text-lg text-primary-400"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= password_input(f, :password_confirmation,
 | 
					    <%= password_input(f, :password_confirmation,
 | 
				
			||||||
@@ -38,12 +34,10 @@
 | 
				
			|||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if Accounts.allow_registration?() do %>
 | 
					    <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
 | 
				
			||||||
      <.link href={Routes.user_registration_path(@conn, :new)} class="btn btn-primary">
 | 
					 | 
				
			||||||
      <%= dgettext("actions", "register") %>
 | 
					      <%= dgettext("actions", "register") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
    <% end %>
 | 
					    <.link href={~p"/users/log_in"} class="btn btn-primary">
 | 
				
			||||||
    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
					 | 
				
			||||||
      <%= dgettext("actions", "log in") %>
 | 
					      <%= dgettext("actions", "log in") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -1,15 +1,16 @@
 | 
				
			|||||||
<div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
					<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
				
			||||||
  <h1 class="title text-primary-400 text-xl">
 | 
					  <h1 class="title text-primary-400 text-xl">
 | 
				
			||||||
    <%= dgettext("actions", "forgot your password?") %>
 | 
					    <%= dgettext("actions", "forgot your password?") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    :let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={:user}
 | 
					    for={%{}}
 | 
				
			||||||
    action={Routes.user_reset_password_path(@conn, :create)}
 | 
					    as={:user}
 | 
				
			||||||
 | 
					    action={~p"/users/reset_password"}
 | 
				
			||||||
    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <%= label(f, :email, class: "title text-lg text-primary-400") %>
 | 
					    <%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
 | 
				
			||||||
    <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
 | 
					    <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= submit(dgettext("actions", "send instructions to reset password"),
 | 
					    <%= submit(dgettext("actions", "send instructions to reset password"),
 | 
				
			||||||
@@ -20,12 +21,10 @@
 | 
				
			|||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if Accounts.allow_registration?() do %>
 | 
					    <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
 | 
				
			||||||
      <.link href={Routes.user_registration_path(@conn, :new)} class="btn btn-primary">
 | 
					 | 
				
			||||||
      <%= dgettext("actions", "register") %>
 | 
					      <%= dgettext("actions", "register") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
    <% end %>
 | 
					    <.link href={~p"/users/log_in"} class="btn btn-primary">
 | 
				
			||||||
    <.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
 | 
					 | 
				
			||||||
      <%= dgettext("actions", "log in") %>
 | 
					      <%= dgettext("actions", "log in") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -5,7 +5,7 @@ defmodule MemexWeb.UserSessionController do
 | 
				
			|||||||
  alias MemexWeb.UserAuth
 | 
					  alias MemexWeb.UserAuth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def new(conn, _params) do
 | 
					  def new(conn, _params) do
 | 
				
			||||||
    render(conn, "new.html", error_message: nil, page_title: gettext("log in"))
 | 
					    render(conn, :new, error_message: nil, page_title: gettext("log in"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create(conn, %{"user" => user_params}) do
 | 
					  def create(conn, %{"user" => user_params}) do
 | 
				
			||||||
@@ -14,7 +14,7 @@ defmodule MemexWeb.UserSessionController do
 | 
				
			|||||||
    if user = Accounts.get_user_by_email_and_password(email, password) do
 | 
					    if user = Accounts.get_user_by_email_and_password(email, password) do
 | 
				
			||||||
      UserAuth.log_in_user(conn, user, user_params)
 | 
					      UserAuth.log_in_user(conn, user, user_params)
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      render(conn, "new.html", error_message: dgettext("errors", "Invalid email or password"))
 | 
					      render(conn, :new, error_message: dgettext("errors", "invalid email or password"))
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								lib/memex_web/controllers/user_session_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/memex_web/controllers/user_session_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.UserSessionHTML do
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					  alias Memex.Accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "user_session_html/*"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
<div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
					<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
 | 
				
			||||||
  <h1 class="title text-primary-400 text-xl">
 | 
					  <h1 class="title text-primary-400 text-xl">
 | 
				
			||||||
    <%= dgettext("actions", "log in") %>
 | 
					    <%= dgettext("actions", "log in") %>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
@@ -6,25 +6,21 @@
 | 
				
			|||||||
  <.form
 | 
					  <.form
 | 
				
			||||||
    :let={f}
 | 
					    :let={f}
 | 
				
			||||||
    for={@conn}
 | 
					    for={@conn}
 | 
				
			||||||
    action={Routes.user_session_path(@conn, :create)}
 | 
					    action={~p"/users/log_in"}
 | 
				
			||||||
    as="user"
 | 
					    as={:user}
 | 
				
			||||||
    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
					    class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <%= if @error_message do %>
 | 
					    <p :if={@error_message} class="alert alert-danger col-span-3">
 | 
				
			||||||
      <div class="alert alert-danger col-span-3">
 | 
					 | 
				
			||||||
        <p>
 | 
					 | 
				
			||||||
      <%= @error_message %>
 | 
					      <%= @error_message %>
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    <% end %>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :email, class: "title text-lg text-primary-400") %>
 | 
					    <%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
 | 
				
			||||||
    <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
 | 
					    <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :password, class: "title text-lg text-primary-400") %>
 | 
					    <%= label(f, :password, gettext("password"), class: "title text-lg text-primary-400") %>
 | 
				
			||||||
    <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
 | 
					    <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <%= label(f, :remember_me, gettext("Keep me logged in for 60 days"),
 | 
					    <%= label(f, :remember_me, gettext("keep me logged in for 60 days"),
 | 
				
			||||||
      class: "title text-lg text-primary-400"
 | 
					      class: "title text-lg text-primary-400"
 | 
				
			||||||
    ) %>
 | 
					    ) %>
 | 
				
			||||||
    <%= checkbox(f, :remember_me, class: "checkbox col-span-2") %>
 | 
					    <%= checkbox(f, :remember_me, class: "checkbox col-span-2") %>
 | 
				
			||||||
@@ -35,12 +31,10 @@
 | 
				
			|||||||
  <hr class="hr" />
 | 
					  <hr class="hr" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-row justify-center items-center space-x-4">
 | 
					  <div class="flex flex-row justify-center items-center space-x-4">
 | 
				
			||||||
    <%= if Accounts.allow_registration?() do %>
 | 
					    <.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
 | 
				
			||||||
      <.link href={Routes.user_registration_path(@conn, :new)} class="btn btn-primary">
 | 
					 | 
				
			||||||
      <%= dgettext("actions", "register") %>
 | 
					      <%= dgettext("actions", "register") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
    <% end %>
 | 
					    <.link href={~p"/users/reset_password"} class="btn btn-primary">
 | 
				
			||||||
    <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
 | 
					 | 
				
			||||||
      <%= dgettext("actions", "forgot your password?") %>
 | 
					      <%= dgettext("actions", "forgot your password?") %>
 | 
				
			||||||
    </.link>
 | 
					    </.link>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -2,12 +2,12 @@ defmodule MemexWeb.UserSettingsController do
 | 
				
			|||||||
  use MemexWeb, :controller
 | 
					  use MemexWeb, :controller
 | 
				
			||||||
  import MemexWeb.Gettext
 | 
					  import MemexWeb.Gettext
 | 
				
			||||||
  alias Memex.Accounts
 | 
					  alias Memex.Accounts
 | 
				
			||||||
  alias MemexWeb.{HomeLive, UserAuth}
 | 
					  alias MemexWeb.UserAuth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  plug :assign_email_and_password_changesets
 | 
					  plug :assign_email_and_password_changesets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def edit(conn, _params) do
 | 
					  def edit(conn, _params) do
 | 
				
			||||||
    render(conn, "edit.html", page_title: gettext("Settings"))
 | 
					    render(conn, :edit, page_title: gettext("settings"))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def update(%{assigns: %{current_user: user}} = conn, %{
 | 
					  def update(%{assigns: %{current_user: user}} = conn, %{
 | 
				
			||||||
@@ -20,7 +20,7 @@ defmodule MemexWeb.UserSettingsController do
 | 
				
			|||||||
        Accounts.deliver_update_email_instructions(
 | 
					        Accounts.deliver_update_email_instructions(
 | 
				
			||||||
          applied_user,
 | 
					          applied_user,
 | 
				
			||||||
          user.email,
 | 
					          user.email,
 | 
				
			||||||
          &Routes.user_settings_url(conn, :confirm_email, &1)
 | 
					          fn token -> url(MemexWeb.Endpoint, ~p"/users/settings/confirm_email/#{token}") end
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
@@ -28,13 +28,13 @@ defmodule MemexWeb.UserSettingsController do
 | 
				
			|||||||
          :info,
 | 
					          :info,
 | 
				
			||||||
          dgettext(
 | 
					          dgettext(
 | 
				
			||||||
            "prompts",
 | 
					            "prompts",
 | 
				
			||||||
            "A link to confirm your email change has been sent to the new address."
 | 
					            "a link to confirm your email change has been sent to the new address."
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
					        |> redirect(to: ~p"/users/settings")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {:error, changeset} ->
 | 
					      {:error, changeset} ->
 | 
				
			||||||
        conn |> render("edit.html", email_changeset: changeset)
 | 
					        conn |> render(:edit, email_changeset: changeset)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -46,12 +46,12 @@ defmodule MemexWeb.UserSettingsController do
 | 
				
			|||||||
    case Accounts.update_user_password(user, password, user_params) do
 | 
					    case Accounts.update_user_password(user, password, user_params) do
 | 
				
			||||||
      {:ok, user} ->
 | 
					      {:ok, user} ->
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
        |> put_flash(:info, dgettext("prompts", "Password updated successfully."))
 | 
					        |> put_flash(:info, dgettext("prompts", "password updated successfully."))
 | 
				
			||||||
        |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
 | 
					        |> put_session(:user_return_to, ~p"/users/settings")
 | 
				
			||||||
        |> UserAuth.log_in_user(user)
 | 
					        |> UserAuth.log_in_user(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {:error, changeset} ->
 | 
					      {:error, changeset} ->
 | 
				
			||||||
        conn |> render("edit.html", password_changeset: changeset)
 | 
					        conn |> render(:edit, password_changeset: changeset)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,11 +62,11 @@ defmodule MemexWeb.UserSettingsController do
 | 
				
			|||||||
    case Accounts.update_user_locale(user, locale) do
 | 
					    case Accounts.update_user_locale(user, locale) do
 | 
				
			||||||
      {:ok, _user} ->
 | 
					      {:ok, _user} ->
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
        |> put_flash(:info, dgettext("prompts", "Language updated successfully."))
 | 
					        |> put_flash(:info, dgettext("prompts", "language updated successfully."))
 | 
				
			||||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
					        |> redirect(to: ~p"/users/settings")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {:error, changeset} ->
 | 
					      {:error, changeset} ->
 | 
				
			||||||
        conn |> render("edit.html", locale_changeset: changeset)
 | 
					        conn |> render(:edit, locale_changeset: changeset)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -74,16 +74,16 @@ defmodule MemexWeb.UserSettingsController do
 | 
				
			|||||||
    case Accounts.update_user_email(user, token) do
 | 
					    case Accounts.update_user_email(user, token) do
 | 
				
			||||||
      :ok ->
 | 
					      :ok ->
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
        |> put_flash(:info, dgettext("prompts", "Email changed successfully."))
 | 
					        |> put_flash(:info, dgettext("prompts", "email changed successfully."))
 | 
				
			||||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
					        |> redirect(to: ~p"/users/settings")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      :error ->
 | 
					      :error ->
 | 
				
			||||||
        conn
 | 
					        conn
 | 
				
			||||||
        |> put_flash(
 | 
					        |> put_flash(
 | 
				
			||||||
          :error,
 | 
					          :error,
 | 
				
			||||||
          dgettext("errors", "Email change link is invalid or it has expired.")
 | 
					          dgettext("errors", "email change link is invalid or it has expired.")
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
					        |> redirect(to: ~p"/users/settings")
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -92,12 +92,12 @@ defmodule MemexWeb.UserSettingsController do
 | 
				
			|||||||
      current_user |> Accounts.delete_user!(current_user)
 | 
					      current_user |> Accounts.delete_user!(current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      conn
 | 
					      conn
 | 
				
			||||||
      |> put_flash(:error, dgettext("prompts", "Your account has been deleted"))
 | 
					      |> put_flash(:error, dgettext("prompts", "your account has been deleted"))
 | 
				
			||||||
      |> redirect(to: Routes.live_path(conn, HomeLive))
 | 
					      |> redirect(to: ~p"/")
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      conn
 | 
					      conn
 | 
				
			||||||
      |> put_flash(:error, dgettext("errors", "Unable to delete user"))
 | 
					      |> put_flash(:error, dgettext("errors", "unable to delete user"))
 | 
				
			||||||
      |> redirect(to: Routes.user_settings_path(conn, :edit))
 | 
					      |> redirect(to: ~p"/users/settings")
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								lib/memex_web/controllers/user_settings_html.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/memex_web/controllers/user_settings_html.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					defmodule MemexWeb.UserSettingsHTML do
 | 
				
			||||||
 | 
					  use MemexWeb, :html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  embed_templates "user_settings_html/*"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user