kern 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +22 -0
  3. data/Gemfile.lock +423 -0
  4. data/README.md +178 -0
  5. data/Rakefile +31 -0
  6. data/app/assets/builds/tailwind.css +2 -0
  7. data/app/controllers/concerns/authentication.rb +55 -0
  8. data/app/controllers/concerns/authentication.rb.tt +55 -0
  9. data/app/controllers/kern/application_controller.rb +7 -0
  10. data/app/controllers/kern/pages_controller.rb +6 -0
  11. data/app/controllers/kern/passwords_controller.rb +40 -0
  12. data/app/controllers/kern/sessions_controller.rb +30 -0
  13. data/app/controllers/kern/settings/users_controller.rb +26 -0
  14. data/app/controllers/kern/settings_controller.rb +6 -0
  15. data/app/controllers/kern/signups_controller.rb +31 -0
  16. data/app/controllers/kern/signups_controller.rb.tt +31 -0
  17. data/app/helpers/kern/component_helper.rb +15 -0
  18. data/app/helpers/kern/turbo_stream_actions_helper.rb +10 -0
  19. data/app/mailers/kern/application_mailer.rb +6 -0
  20. data/app/mailers/kern/passwords_mailer.rb +9 -0
  21. data/app/models/actor.rb +4 -0
  22. data/app/models/application_form.rb +12 -0
  23. data/app/models/current.rb +8 -0
  24. data/app/models/member/acting.rb +28 -0
  25. data/app/models/member/setup.rb +37 -0
  26. data/app/models/member.rb +10 -0
  27. data/app/models/role.rb +5 -0
  28. data/app/models/session.rb +5 -0
  29. data/app/models/signup.rb +38 -0
  30. data/app/models/user/workspace_member.rb +12 -0
  31. data/app/models/user.rb +14 -0
  32. data/app/models/workspace/members.rb +10 -0
  33. data/app/models/workspace/setup.rb +21 -0
  34. data/app/models/workspace.rb +7 -0
  35. data/app/views/components/_container.html.erb +2 -0
  36. data/app/views/components/_flash.html.erb +7 -0
  37. data/app/views/components/_heading.html.erb +12 -0
  38. data/app/views/components/flash/_message.html.erb +4 -0
  39. data/app/views/kern/pages/welcome.html.erb +42 -0
  40. data/app/views/kern/passwords/edit.html.erb +12 -0
  41. data/app/views/kern/passwords/new.html.erb +10 -0
  42. data/app/views/kern/passwords_mailer/reset.html.erb +6 -0
  43. data/app/views/kern/passwords_mailer/reset.html.erb.tt +6 -0
  44. data/app/views/kern/passwords_mailer/reset.text.erb +4 -0
  45. data/app/views/kern/passwords_mailer/reset.text.erb.tt +4 -0
  46. data/app/views/kern/sessions/new.html.erb +16 -0
  47. data/app/views/kern/settings/_cards.html.erb +10 -0
  48. data/app/views/kern/settings/show.html.erb +5 -0
  49. data/app/views/kern/settings/users/show.html.erb +15 -0
  50. data/app/views/kern/signups/new.html.erb +16 -0
  51. data/app/views/layouts/kern/application/_navigation.html.erb +30 -0
  52. data/app/views/layouts/kern/application.html.erb +31 -0
  53. data/app/views/layouts/kern/application.html.erb.tt +31 -0
  54. data/app/views/layouts/kern/auth.html.erb +42 -0
  55. data/bin/dev +4 -0
  56. data/bin/rails +14 -0
  57. data/bin/release +35 -0
  58. data/config/routes.rb +13 -0
  59. data/db/migrate/20250101000001_create_users.rb +13 -0
  60. data/db/migrate/20250101000002_create_sessions.rb +11 -0
  61. data/db/migrate/20250101000003_create_workspaces.rb +12 -0
  62. data/db/migrate/20250101000004_create_members.rb +15 -0
  63. data/db/migrate/20250101000005_create_roles.rb +11 -0
  64. data/db/migrate/20250101000006_create_actors.rb +10 -0
  65. data/kern.gemspec +22 -0
  66. data/lib/generators/kern/feature/USAGE +17 -0
  67. data/lib/generators/kern/feature/feature_generator.rb +77 -0
  68. data/lib/generators/kern/helpers.rb +28 -0
  69. data/lib/generators/kern/install/USAGE +13 -0
  70. data/lib/generators/kern/install/install_generator.rb +48 -0
  71. data/lib/generators/kern/install/templates/configurations/README.md +33 -0
  72. data/lib/generators/kern/install/templates/configurations/urls.yml +9 -0
  73. data/lib/generators/kern/views/USAGE +22 -0
  74. data/lib/generators/kern/views/views_generator.rb +42 -0
  75. data/lib/kern/config.rb +25 -0
  76. data/lib/kern/engine.rb +17 -0
  77. data/lib/kern/form_builder/input.rb +100 -0
  78. data/lib/kern/form_builder/styles.rb +53 -0
  79. data/lib/kern/form_builder.rb +101 -0
  80. data/lib/kern/version.rb +3 -0
  81. data/lib/kern.rb +7 -0
  82. data/lib/sluggable.rb +66 -0
  83. data/lib/tasks/kern_tasks.rake +4 -0
  84. metadata +139 -0
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.1.16 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-600:oklch(57.7% .245 27.325);--color-emerald-300:oklch(84.5% .143 164.978);--color-cyan-400:oklch(78.9% .154 211.53);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-pink-200:oklch(89.9% .061 343.231);--color-pink-800:oklch(45.9% .187 3.815);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:var(--color-slate-50);--color-gray-100:var(--color-slate-100);--color-gray-200:var(--color-slate-200);--color-gray-300:var(--color-slate-300);--color-gray-400:var(--color-slate-400);--color-gray-500:var(--color-slate-500);--color-gray-600:var(--color-slate-600);--color-gray-700:var(--color-slate-700);--color-gray-800:var(--color-slate-800);--color-gray-900:var(--color-slate-900);--color-gray-950:var(--color-slate-950);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--tracking-tight:-.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4,0,.2,1);--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-brand-50:var(--color-blue-50);--color-brand-300:var(--color-blue-300);--color-brand-400:var(--color-blue-400);--color-brand-500:var(--color-blue-500);--color-brand-600:var(--color-blue-600);--color-brand-700:var(--color-blue-700);--color-brand-800:var(--color-blue-800);--color-brand-950:var(--color-blue-950)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}a:not([class]){text-decoration-line:underline}a:not([class]):hover{text-decoration-line:none}}@layer components{[class^=btn-]{align-items:center;column-gap:calc(var(--spacing)*1);width:100%;padding-inline:calc(var(--spacing)*3.5);padding-block:calc(var(--spacing)*1.25);display:flex}@media (min-width:48rem){[class^=btn-]{width:auto}}[class^=btn-]{text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:.0125em;letter-spacing:.0125em;border-style:var(--tw-border-style);border-radius:var(--radius-sm);--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:transparent;cursor:pointer;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-width:1px;border-color:#0000}[class^=btn-] [data-slot=icon],[class^=btn-] svg{width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.btn-secondary{color:var(--color-gray-800);border-color:var(--color-white);--tw-ring-color:var(--color-gray-300)}.btn-secondary [data-slot=icon],.btn-secondary svg{color:var(--color-gray-600)}.btn-secondary:hover{--tw-ring-color:var(--color-gray-300)}.btn-primary{background-color:var(--color-brand-600);color:var(--color-white)}.btn-primary [data-slot=icon],.btn-primary svg{color:var(--color-white)}.btn-primary:hover{background-color:var(--color-brand-700)}.btn-block{width:100%;display:block}.buttons{margin-top:calc(var(--spacing)*4);justify-content:flex-end;display:flex}}@layer utilities{.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.-top-\[400px\]{top:-400px}.top-0{top:calc(var(--spacing)*0)}.-right-\[300px\]{right:-300px}.right-4{right:calc(var(--spacing)*4)}.-bottom-\[450px\]{bottom:-450px}.bottom-4{bottom:calc(var(--spacing)*4)}.-left-\[350px\]{left:-350px}.col-span-12{grid-column:span 12/span 12}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-7{margin-top:calc(var(--spacing)*7)}.mt-8{margin-top:calc(var(--spacing)*8)}.ml-0{margin-left:calc(var(--spacing)*0)}.ml-1{margin-left:calc(var(--spacing)*1)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline{display:inline}.size-3\.5{width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-4\.5{width:calc(var(--spacing)*4.5);height:calc(var(--spacing)*4.5)}.size-5{width:calc(var(--spacing)*5);height:calc(var(--spacing)*5)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.size-\[800px\]{width:800px;height:800px}.h-8{height:calc(var(--spacing)*8)}.h-dvh{height:100dvh}.h-screen{height:100vh}.w-16{width:calc(var(--spacing)*16)}.w-full{width:100%}.max-w-5xl{max-width:var(--container-5xl)}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-x-0{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.rotate-32{rotate:32deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.place-items-center{place-items:center}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-x-1{column-gap:calc(var(--spacing)*1)}.gap-x-1\.5{column-gap:calc(var(--spacing)*1.5)}.gap-x-2{column-gap:calc(var(--spacing)*2)}.gap-y-0\.5{row-gap:calc(var(--spacing)*.5)}.gap-y-5{row-gap:calc(var(--spacing)*5)}.overflow-clip{overflow:clip}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200\/50{border-color:#e2e8f080}@supports (color:color-mix(in lab, red, red)){.border-gray-200\/50{border-color:color-mix(in oklab,var(--color-gray-200)50%,transparent)}}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-200\/40{background-color:#e2e8f066}@supports (color:color-mix(in lab, red, red)){.bg-gray-200\/40{background-color:color-mix(in oklab,var(--color-gray-200)40%,transparent)}}.bg-white{background-color:var(--color-white)}.bg-white\/50{background-color:#ffffff80}@supports (color:color-mix(in lab, red, red)){.bg-white\/50{background-color:color-mix(in oklab,var(--color-white)50%,transparent)}}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-gray-900\/80{--tw-gradient-from:#0f172bcc}@supports (color:color-mix(in lab, red, red)){.from-gray-900\/80{--tw-gradient-from:color-mix(in oklab,var(--color-gray-900)80%,transparent)}}.from-gray-900\/80{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.fill-brand-400{fill:var(--color-brand-400)}.fill-brand-600\/60{fill:#155dfc99}@supports (color:color-mix(in lab, red, red)){.fill-brand-600\/60{fill:color-mix(in oklab,var(--color-brand-600)60%,transparent)}}.p-2{padding:calc(var(--spacing)*2)}.px-0\.5{padding-inline:calc(var(--spacing)*.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-5{padding-block:calc(var(--spacing)*5)}.pt-4{padding-top:calc(var(--spacing)*4)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.text-center{text-align:center}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.text-brand-500{color:var(--color-brand-500)}.text-brand-600{color:var(--color-brand-600)}.text-current{color:currentColor}.text-cyan-400{color:var(--color-cyan-400)}.text-emerald-300{color:var(--color-emerald-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-gray-950{color:var(--color-gray-950)}.text-red-400{color:var(--color-red-400)}.text-red-600{color:var(--color-red-600)}.text-slate-800{color:var(--color-slate-800)}.text-white{color:var(--color-white)}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-brand-500{accent-color:var(--color-brand-500)}.opacity-100{opacity:1}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-gray-200\/50{--tw-ring-color:#e2e8f080}@supports (color:color-mix(in lab, red, red)){.ring-gray-200\/50{--tw-ring-color:color-mix(in oklab,var(--color-gray-200)50%,transparent)}}.ring-gray-200\/60{--tw-ring-color:#e2e8f099}@supports (color:color-mix(in lab, red, red)){.ring-gray-200\/60{--tw-ring-color:color-mix(in oklab,var(--color-gray-200)60%,transparent)}}.ring-gray-200\/80{--tw-ring-color:#e2e8f0cc}@supports (color:color-mix(in lab, red, red)){.ring-gray-200\/80{--tw-ring-color:color-mix(in oklab,var(--color-gray-200)80%,transparent)}}.ring-gray-700\/90{--tw-ring-color:#314158e6}@supports (color:color-mix(in lab, red, red)){.ring-gray-700\/90{--tw-ring-color:color-mix(in oklab,var(--color-gray-700)90%,transparent)}}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.delay-75{transition-delay:75ms}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}:is(.\*\:inline-block>*){display:inline-block}@media (hover:hover){.group-hover\/link\:fill-brand-700:is(:where(.group\/link):hover *){fill:var(--color-brand-700)}.group-hover\/link\:text-brand-700:is(:where(.group\/link):hover *){color:var(--color-brand-700)}.group-hover\/link\:text-brand-950:is(:where(.group\/link):hover *){color:var(--color-brand-950)}.group-hover\/link\:text-gray-950:is(:where(.group\/link):hover *){color:var(--color-gray-950)}}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.marker\:text-blue-500 ::marker{color:var(--color-blue-500)}.marker\:text-blue-500::marker{color:var(--color-blue-500)}.marker\:text-blue-500 ::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-blue-500::-webkit-details-marker{color:var(--color-blue-500)}.selection\:bg-pink-200\/40 ::selection{background-color:#fccee866}@supports (color:color-mix(in lab, red, red)){.selection\:bg-pink-200\/40 ::selection{background-color:color-mix(in oklab,var(--color-pink-200)40%,transparent)}}.selection\:bg-pink-200\/40::selection{background-color:#fccee866}@supports (color:color-mix(in lab, red, red)){.selection\:bg-pink-200\/40::selection{background-color:color-mix(in oklab,var(--color-pink-200)40%,transparent)}}.selection\:text-pink-800\/70 ::selection{color:#a2004cb3}@supports (color:color-mix(in lab, red, red)){.selection\:text-pink-800\/70 ::selection{color:color-mix(in oklab,var(--color-pink-800)70%,transparent)}}.selection\:text-pink-800\/70::selection{color:#a2004cb3}@supports (color:color-mix(in lab, red, red)){.selection\:text-pink-800\/70::selection{color:color-mix(in oklab,var(--color-pink-800)70%,transparent)}}.file\:mr-2::file-selector-button{margin-right:calc(var(--spacing)*2)}.file\:rounded-full::file-selector-button{border-radius:3.40282e38px}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-gray-100::file-selector-button{background-color:var(--color-gray-100)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing)*4)}.file\:py-1::file-selector-button{padding-block:calc(var(--spacing)*1)}.file\:text-xs::file-selector-button{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-gray-700::file-selector-button{color:var(--color-gray-700)}@media (hover:hover){.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.hover\:bg-brand-50:hover{background-color:var(--color-brand-50)}.hover\:bg-gray-200\/40:hover{background-color:#e2e8f066}@supports (color:color-mix(in lab, red, red)){.hover\:bg-gray-200\/40:hover{background-color:color-mix(in oklab,var(--color-gray-200)40%,transparent)}}.hover\:text-brand-800:hover{color:var(--color-brand-800)}.hover\:text-gray-500:hover{color:var(--color-gray-500)}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-lg\/5:hover{--tw-shadow-alpha:5%;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,oklab(0% 0 0/.05)),0 4px 6px -4px var(--tw-shadow-color,oklab(0% 0 0/.05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-gray-300:hover{--tw-ring-color:var(--color-gray-300)}.hover\:file\:bg-gray-200:hover::file-selector-button{background-color:var(--color-gray-200)}}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:ring:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-brand-300:focus{--tw-ring-color:var(--color-brand-300)}.focus\:ring-gray-300:focus{--tw-ring-color:var(--color-gray-300)}.focus\:ring-offset-1:focus{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-25:disabled{opacity:.25}.disabled\:opacity-50:disabled{opacity:.5}@media (hover:hover){.disabled\:hover\:ring-gray-200\/80:disabled:hover{--tw-ring-color:#e2e8f0cc}@supports (color:color-mix(in lab, red, red)){.disabled\:hover\:ring-gray-200\/80:disabled:hover{--tw-ring-color:color-mix(in oklab,var(--color-gray-200)80%,transparent)}}}@media (min-width:48rem){.md\:col-span-4{grid-column:span 4/span 4}.md\:mx-4{margin-inline:calc(var(--spacing)*4)}.md\:rounded-md{border-radius:var(--radius-md)}.md\:rounded-xl{border-radius:var(--radius-xl)}.md\:px-4{padding-inline:calc(var(--spacing)*4)}.md\:pb-4{padding-bottom:calc(var(--spacing)*4)}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.md\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.md\:ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}@media (min-width:64rem){.lg\:not-sr-only{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:w-60{width:calc(var(--spacing)*60)}.lg\:px-6{padding-inline:calc(var(--spacing)*6)}}@starting-style{.starting\:ml-3\.5{margin-left:calc(var(--spacing)*3.5)}}@starting-style{.starting\:translate-x-4{--tw-translate-x:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}}@starting-style{.starting\:scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}}@starting-style{.starting\:opacity-0{opacity:0}}.\[\&\>input\]\:mt-\[0\.15em\]>input{margin-top:.15em}.\[\&\[data-slot\=\'field\'\]\+\[data-slot\=\'field\'\]\]\:mt-4[data-slot=field]+[data-slot=field]{margin-top:calc(var(--spacing)*4)}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
@@ -0,0 +1,55 @@
1
+ module Authentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :require_authentication
6
+ helper_method :authenticated?
7
+ end
8
+
9
+ class_methods do
10
+ def allow_unauthenticated_access(**options)
11
+ skip_before_action :require_authentication, **options
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def authenticated?
18
+ resume_session
19
+ end
20
+
21
+ def require_authentication
22
+ resume_session || request_authentication
23
+ end
24
+
25
+ def resume_session
26
+ Current.session ||= find_session_by_cookie
27
+ end
28
+
29
+ def find_session_by_cookie
30
+ Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
31
+ end
32
+
33
+ def request_authentication
34
+ session[:return_to_after_authenticating] = request.url
35
+
36
+ redirect_to kern.new_session_path, notice: "You need to be logged in"
37
+ end
38
+
39
+ def after_authentication_url
40
+ session.delete(:return_to_after_authenticating) || root_url
41
+ end
42
+
43
+ def start_new_session_for(user)
44
+ user.sessions.create!(user_agent: request.user_agent.presence || "[No User Agent]", ip_address: request.remote_ip).tap do |session|
45
+ Current.session = session
46
+ cookies.signed.permanent[:session_id] = {value: session.id, httponly: true, same_site: :lax}
47
+ end
48
+ end
49
+
50
+ def terminate_session
51
+ Current.session.destroy
52
+
53
+ cookies.delete(:session_id)
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ module Authentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :require_authentication
6
+ helper_method :authenticated?
7
+ end
8
+
9
+ class_methods do
10
+ def allow_unauthenticated_access(**options)
11
+ skip_before_action :require_authentication, **options
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def authenticated?
18
+ resume_session
19
+ end
20
+
21
+ def require_authentication
22
+ resume_session || request_authentication
23
+ end
24
+
25
+ def resume_session
26
+ Current.session ||= find_session_by_cookie
27
+ end
28
+
29
+ def find_session_by_cookie
30
+ Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
31
+ end
32
+
33
+ def request_authentication
34
+ session[:return_to_after_authenticating] = request.url
35
+
36
+ redirect_to new_session_path, notice: "You need to be logged in"
37
+ end
38
+
39
+ def after_authentication_url
40
+ session.delete(:return_to_after_authenticating) || root_url
41
+ end
42
+
43
+ def start_new_session_for(user)
44
+ user.sessions.create!(user_agent: request.user_agent.presence || "[No User Agent]", ip_address: request.remote_ip).tap do |session|
45
+ Current.session = session
46
+ cookies.signed.permanent[:session_id] = {value: session.id, httponly: true, same_site: :lax}
47
+ end
48
+ end
49
+
50
+ def terminate_session
51
+ Current.session.destroy
52
+
53
+ cookies.delete(:session_id)
54
+ end
55
+ end
@@ -0,0 +1,7 @@
1
+ module Kern
2
+ class ApplicationController < ActionController::Base
3
+ include Authentication
4
+
5
+ default_form_builder Kern::FormBuilder
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Kern
2
+ class PagesController < ApplicationController
3
+ def welcome
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,40 @@
1
+ module Kern
2
+ class PasswordsController < ApplicationController
3
+ allow_unauthenticated_access
4
+
5
+ before_action :redirect_to_root, if: -> { authenticated? }, only: %w[new create]
6
+ before_action :set_user_by_token, only: %w[edit update]
7
+
8
+ layout "kern/auth"
9
+
10
+ def new
11
+ end
12
+
13
+ def create
14
+ if (user = User.find_by(email_address: params[:email_address]))
15
+ PasswordsMailer.reset(user).deliver_later
16
+ end
17
+
18
+ redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)"
19
+ end
20
+
21
+ def edit
22
+ end
23
+
24
+ def update
25
+ if @user.update(params.permit(:password, :password_confirmation))
26
+ redirect_to new_session_path, notice: "Password has been reset"
27
+ else
28
+ redirect_to edit_password_path(params[:token]), alert: "Passwords did not match"
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def set_user_by_token
35
+ @user = User.find_by_password_reset_token!(params[:token])
36
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
37
+ redirect_to new_password_path, alert: "Password reset link is invalid or has expired"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,30 @@
1
+ module Kern
2
+ class SessionsController < ApplicationController
3
+ allow_unauthenticated_access only: %w[new create]
4
+
5
+ before_action :redirect_to_root, if: -> { authenticated? }, only: %w[new create]
6
+
7
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later" }
8
+
9
+ layout "kern/auth"
10
+
11
+ def new
12
+ end
13
+
14
+ def create
15
+ if (user = User.authenticate_by(params.permit(:email_address, :password)))
16
+ start_new_session_for user
17
+
18
+ redirect_to after_authentication_url
19
+ else
20
+ redirect_to new_session_path, alert: "Try another email address or password"
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ terminate_session
26
+
27
+ redirect_to new_session_path
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ module Kern
2
+ class Settings::UsersController < ApplicationController
3
+ before_action :set_user, only: %w[show update]
4
+
5
+ def show
6
+ end
7
+
8
+ def update
9
+ if @user.update(user_params)
10
+ redirect_to settings_user_path, notice: "Updated"
11
+ else
12
+ redirect_to settings_user_path, alert: "Password is incorrect"
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def set_user
19
+ @user = Current.user
20
+ end
21
+
22
+ def user_params
23
+ params.expect(user: [:email_address, :password, :password_confirmation])
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,6 @@
1
+ module Kern
2
+ class SettingsController < ApplicationController
3
+ def show
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ module Kern
2
+ class SignupsController < ApplicationController
3
+ allow_unauthenticated_access only: %w[new create]
4
+
5
+ before_action :redirect_to_root, if: -> { authenticated? }, only: %w[new create]
6
+
7
+ layout "kern/auth"
8
+
9
+ def new
10
+ @signup = Signup.new
11
+ end
12
+
13
+ def create
14
+ @signup = Signup.new(signup_params)
15
+
16
+ if @signup.save
17
+ start_new_session_for @signup.user
18
+
19
+ redirect_to main_app.root_path
20
+ else
21
+ render :new, status: :unprocessable_entity
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def signup_params
28
+ params.expect(signup: [:email_address, :password])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module Kern
2
+ class SignupsController < ApplicationController
3
+ allow_unauthenticated_access only: %w[new create]
4
+
5
+ before_action :redirect_to_root, if: -> { authenticated? }, only: %w[new create]
6
+
7
+ layout "kern/auth"
8
+
9
+ def new
10
+ @signup = Signup.new
11
+ end
12
+
13
+ def create
14
+ @signup = Signup.new(signup_params)
15
+
16
+ if @signup.save
17
+ start_new_session_for @signup.user
18
+
19
+ redirect_to root_path
20
+ else
21
+ render :new, status: :unprocessable_entity
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def signup_params
28
+ params.expect(signup: [:email_address, :password])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # Example:
2
+ # <%= component "card", locals: { title: "My Card" } do %>
3
+ # <p>This is the card content.</p>
4
+ # <% end %>
5
+ #
6
+ # This will render `app/views/components/_card.html.erb` and pass the block's
7
+ # content to the `yield` statement within the card partial.
8
+ #
9
+ module Kern
10
+ module ComponentHelper
11
+ def component(name, locals = {}, &block)
12
+ render(layout: "components/#{name}", locals: locals) { block_given? ? capture(&block) : "" }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Kern
2
+ module TurboStreamActionsHelper
3
+ def notify(message, type: "notice")
4
+ append("flash") { @view_context.render "components/flash/message", type: type, message: message }
5
+ end
6
+ alias_method :notify_with, :notify
7
+ end
8
+ end
9
+
10
+ Turbo::Streams::TagBuilder.prepend(Kern::TurboStreamActionsHelper)
@@ -0,0 +1,6 @@
1
+ module Kern
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Kern
2
+ class PasswordsMailer < ApplicationMailer
3
+ def reset(user)
4
+ @user = user
5
+
6
+ mail subject: "Reset your password", to: user.email_address
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ class Actor < ApplicationRecord
2
+ belongs_to :member
3
+ belongs_to :role
4
+ end
@@ -0,0 +1,12 @@
1
+ class ApplicationForm
2
+ include ActiveModel::Model
3
+ include ActiveModel::Attributes
4
+ include ActiveModel::Attributes::Normalization
5
+ include ActiveModel::Validations::Callbacks
6
+
7
+ def model_name
8
+ ActiveModel::Name.new(self, nil, self.class.name.delete_suffix("Form"))
9
+ end
10
+
11
+ def transaction(&block) = ActiveRecord::Base.transaction(&block)
12
+ end
@@ -0,0 +1,8 @@
1
+ class Current < ActiveSupport::CurrentAttributes
2
+ attribute :session
3
+
4
+ delegate :user, to: :session, allow_nil: true
5
+ delegate :workspace, to: :user
6
+
7
+ def member = user.members.find_by(workspace: workspace)
8
+ end
@@ -0,0 +1,28 @@
1
+ module Member::Acting
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :actors, dependent: :destroy
6
+ has_many :roles, through: :actors
7
+ end
8
+
9
+ def has_role?(role_name)
10
+ roles.any? { it.name.underscore.to_sym == role_name }
11
+ end
12
+
13
+ def add_role(role_name)
14
+ role = Role.where(name: role_name.to_s).first_or_create
15
+
16
+ raise "Invalid role" if !role
17
+
18
+ actors.create role: role
19
+ end
20
+
21
+ def remove_role(role_name)
22
+ role = Role.find_by(name: role_name.to_s)
23
+
24
+ raise "Invalid role" if !role
25
+
26
+ actors.where(role: role).destroy_all
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ class Member::Setup
2
+ def initialize(workspace:, user:, role: "member")
3
+ @workspace = workspace
4
+ @user = user
5
+ @role = role&.to_sym
6
+ end
7
+
8
+ def save
9
+ ActiveRecord::Base.transaction do
10
+ create_member.tap do |member|
11
+ add_roles_to member
12
+
13
+ mark_workspace_current
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def create_member = Member.create(workspace: @workspace, user: @user)
21
+
22
+ def add_roles_to(member)
23
+ roles.each do |role_name|
24
+ member.actors.create role: Role.where(name: role_name).first_or_create
25
+ end
26
+ end
27
+
28
+ def mark_workspace_current = @user.update!(workspace: @workspace)
29
+
30
+ def roles
31
+ {
32
+ administrator: %w[administrator],
33
+ member: %w[member],
34
+ owner: %w[administrator member owner]
35
+ }[@role] || []
36
+ end
37
+ end
@@ -0,0 +1,10 @@
1
+ class Member < ApplicationRecord
2
+ include Sluggable
3
+
4
+ include Acting
5
+
6
+ belongs_to :user
7
+ belongs_to :workspace
8
+
9
+ delegate :email_address, to: :user
10
+ end
@@ -0,0 +1,5 @@
1
+ class Role < ApplicationRecord
2
+ AVAILABLE = %w[administrator member owner]
3
+
4
+ validates :name, presence: true, inclusion: {in: AVAILABLE}, uniqueness: true
5
+ end
@@ -0,0 +1,5 @@
1
+ class Session < ApplicationRecord
2
+ belongs_to :user
3
+
4
+ # encrypts :user_agent, :ip
5
+ end
@@ -0,0 +1,38 @@
1
+ class Signup < ApplicationForm
2
+ attribute :email_address, :string
3
+ attribute :password, :string
4
+
5
+ normalizes :email_address, with: -> { it.strip.downcase }
6
+
7
+ validates :email_address, :password, presence: true
8
+ validates_format_of :email_address, with: URI::MailTo::EMAIL_REGEXP
9
+ validate :email_is_unique?
10
+
11
+ validates :password, length: 8..128
12
+
13
+ def save
14
+ if valid?
15
+ transaction do
16
+ @user = create_user.tap { it.setup_workspace.save }.tap do |user|
17
+ # Add other actions here, e.g. `send_welcome_email_to user`
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ attr_reader :user
24
+
25
+ private
26
+
27
+ def create_user = User.create!(email_address: email_address, password: password)
28
+
29
+ # def send_welcome_email_to(user)
30
+ # Logic to send email
31
+ # end
32
+
33
+ def email_is_unique?
34
+ if User.exists? email_address: email_address
35
+ errors.add :email_address, "has already been taken"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ module User::WorkspaceMember
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :members, dependent: :destroy
6
+ has_many :workspaces, through: :members
7
+
8
+ belongs_to :workspace, class_name: "Workspace", foreign_key: :current_workspace_id, optional: true
9
+ end
10
+
11
+ def setup_workspace = Workspace::Setup.new(user: self)
12
+ end
@@ -0,0 +1,14 @@
1
+ class User < ApplicationRecord
2
+ include WorkspaceMember
3
+
4
+ has_secure_password
5
+ has_many :sessions, dependent: :destroy
6
+
7
+ normalizes :email_address, with: -> { it.strip.downcase }
8
+
9
+ validates_format_of :email_address, with: URI::MailTo::EMAIL_REGEXP
10
+
11
+ validates :password, length: 8..128
12
+
13
+ # encrypts :email, deterministic: true, downcase: true
14
+ end
@@ -0,0 +1,10 @@
1
+ module Workspace::Members
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :members, dependent: :destroy
6
+ has_many :users, through: :members
7
+ end
8
+
9
+ def add_member(to:, role: nil) = Member::Setup.new(workspace: self, user: to, role: role).save
10
+ end
@@ -0,0 +1,21 @@
1
+ class Workspace::Setup
2
+ def initialize(user:)
3
+ @user = user
4
+
5
+ raise ArgumentError.new("You need to pass a user, eg. `user: …`") if !user
6
+ end
7
+
8
+ def save
9
+ ActiveRecord::Base.transaction do
10
+ create_workspace.tap do |workspace|
11
+ workspace.add_member to: @user, role: :owner
12
+ end
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def create_workspace
19
+ Workspace.create(name: "My Workspace")
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ class Workspace < ApplicationRecord
2
+ include Sluggable
3
+
4
+ include Members
5
+
6
+ validates :name, presence: true
7
+ end
@@ -0,0 +1,2 @@
1
+ <%# locals: (additional_css: nil, css: "mt-8 px-2 max-w-5xl mx-auto") %>
2
+ <%= tag.main yield, class: class_names(css, additional_css) %>
@@ -0,0 +1,7 @@
1
+ <ul id="flash" class="flex flex-col gap-1 fixed right-4 bottom-4 max-w-md">
2
+ <% flash.each do |type, messages| %>
3
+ <% Array(messages).each do |message| %>
4
+ <%= component "flash/message", type:, message: %>
5
+ <% end %>
6
+ <% end %>
7
+ </ul>