kern 0.5.0 → 0.6.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +13 -2
  4. data/README.md +15 -13
  5. data/app/assets/builds/tailwind.css +1 -1
  6. data/app/controllers/kern/billing_profiles/subscriptions_controller.rb +37 -0
  7. data/app/controllers/kern/billing_profiles/subscriptions_controller.rb.tt +37 -0
  8. data/app/controllers/kern/settings/subscriptions_controller.rb +6 -0
  9. data/app/controllers/kern/signups_controller.rb +1 -1
  10. data/app/models/billing_profile/subscription.rb +16 -0
  11. data/app/models/billing_profile.rb +7 -0
  12. data/app/models/session.rb +1 -1
  13. data/app/models/signup.rb +1 -1
  14. data/app/models/user.rb +1 -1
  15. data/app/models/workspace/access.rb +3 -0
  16. data/app/models/workspace/access.rb.tt +10 -0
  17. data/app/models/workspace/billable.rb +16 -0
  18. data/app/models/workspace/feature_access.rb +28 -0
  19. data/app/models/workspace/members.rb +9 -7
  20. data/app/models/workspace/setup.rb +5 -3
  21. data/app/models/workspace.rb +2 -0
  22. data/app/views/components/_dialog.html.erb +36 -0
  23. data/app/views/kern/pages/welcome.html.erb +10 -6
  24. data/app/views/kern/settings/_cards.html.erb +11 -3
  25. data/app/views/kern/settings/subscriptions/_plan.html.erb +35 -0
  26. data/app/views/kern/settings/subscriptions/show.html.erb +11 -0
  27. data/app/views/kern/settings/users/show.html.erb +2 -2
  28. data/app/views/kern/signups/new.html.erb +2 -0
  29. data/app/views/layouts/kern/application.html.erb +3 -1
  30. data/app/views/layouts/kern/application.html.erb.tt +1 -0
  31. data/app/views/layouts/kern/auth.html.erb +1 -1
  32. data/app/webhooks/stripe/base.rb +31 -0
  33. data/app/webhooks/stripe/checkout_session_completed.rb +46 -0
  34. data/app/webhooks/stripe/customer_subscription_deleted.rb +20 -0
  35. data/app/webhooks/stripe/customer_subscription_updated.rb +33 -0
  36. data/bin/release +2 -3
  37. data/config/initializers/stripe.rb +5 -0
  38. data/config/initializers/stripe.rb.tt +3 -0
  39. data/config/routes.rb +4 -0
  40. data/db/migrate/20250101000007_create_billing_profiles.rb +14 -0
  41. data/db/migrate/20250101000008_create_billing_profile_subscriptions.rb +20 -0
  42. data/lib/generators/kern/feature/USAGE +1 -0
  43. data/lib/generators/kern/feature/feature_generator.rb +82 -19
  44. data/lib/generators/kern/install/install_generator.rb +43 -7
  45. data/lib/generators/kern/install/templates/POST_INSTALL +25 -0
  46. data/lib/generators/kern/install/templates/configurations/plans.yml +43 -0
  47. data/lib/generators/kern/install/templates/configurations/stripe.yml +11 -0
  48. data/lib/generators/kern/layouts/layouts_generator.rb +12 -0
  49. data/lib/kern/version.rb +1 -1
  50. metadata +25 -3
  51. data/lib/generators/kern/views/USAGE +0 -22
  52. data/lib/generators/kern/views/views_generator.rb +0 -42
@@ -4,7 +4,7 @@
4
4
  </h1>
5
5
 
6
6
  <p class="mt-2 text-sm md:text-base text-gray-800">
7
- SaaS foundation for Ruby on Rails apps by <%= link_to "Rails Designer", "https://railsdesigner.com" %>. You are ready to build your next SaaS.
7
+ SaaS foundation for Ruby on Rails apps by <%= link_to "Rails Designer", "https://railsdesigner.com" %>. You are ready to build your next SaaS. 🎉
8
8
  </p>
9
9
 
10
10
  <section class="mt-6">
@@ -12,21 +12,25 @@
12
12
  Get started
13
13
  </h2>
14
14
 
15
- <ul class="list-disc list-inside marker:text-blue-500">
15
+ <ul class="list-disc list-inside marker:text-blue-500 [&_code]:font-bold">
16
16
  <li>
17
- add your own root route (<code>root to: "dashboard#show"</code>)
17
+ add your own root route (<code class="select-all">root to: "dashboard#show"</code>)
18
18
  </li>
19
19
 
20
20
  <li>
21
- install <%= link_to "Rails Icons", "https://github.com/Rails-Designer/rails_icons" %>; Kern uses the <b>Phosphor</b> library
21
+ install <%= link_to "Rails Icons", "https://github.com/Rails-Designer/rails_icons" %>; Kern uses the <b>Phosphor</b> library (but you can choose any other library)
22
22
  </li>
23
23
 
24
24
  <li>
25
- override any views by running <code>bin/rails g kern:views</code>
25
+ override any views by running <code class="select-all">bin/rails g kern:views</code>
26
26
  </li>
27
27
 
28
28
  <li>
29
- override any feature by running <code>bin/rals g kern:feature</code>
29
+ override any feature by running <code class="select-all">bin/rals g kern:feature</code>
30
+ </li>
31
+
32
+ <li>
33
+ check out <%= link_to "Rails Designer's UI Components", "https://railsdesigner.com/components/" %>
30
34
  </li>
31
35
 
32
36
  <li>
@@ -1,10 +1,18 @@
1
- <%# locals: (card_css: "col-span-12 md:col-span-4 lg:col-span-3", link_css: "block px-3 py-2 border border-gray-100 rounded-md transition ease-in-out delay-75 hover:border-gray-200 hover:shadow-lg/5", heading_css: "text-base font-bold tracking-tight text-gray-800", description_css: "mt-0.5 text-sm font-light text-gray-600") %>
2
- <ul class="grid grid-cols-12">
1
+ <%# locals: (card_css: "col-span-12 md:col-span-4 lg:col-span-3", link_css: "block px-3 py-2 border border-gray-100 rounded-md transition ease-in-out delay-75 hover:border-gray-200", heading_css: "text-base font-bold tracking-tight text-gray-800", description_css: "mt-0.5 text-sm font-light text-gray-600") %>
2
+ <ul class="grid grid-cols-12 gap-8">
3
3
  <%= tag.li class: card_css do %>
4
4
  <%= link_to settings_user_path, class: link_css do %>
5
- <%= tag.h4 "Account", class: heading_css %>
5
+ <%= tag.h4 "User settings", class: heading_css %>
6
6
 
7
7
  <%= tag.p "Update your email and password", class: description_css %>
8
8
  <% end %>
9
9
  <% end %>
10
+
11
+ <%= tag.li class: card_css do %>
12
+ <%= link_to settings_subscriptions_path, class: link_css do %>
13
+ <%= tag.h4 "Subscription", class: heading_css %>
14
+
15
+ <%= tag.p "Edit subscription, manage billing and invoices", class: description_css %>
16
+ <% end %>
17
+ <% end %>
10
18
  </ul>
@@ -0,0 +1,35 @@
1
+ <%# locals: (plan:) %>
2
+ <li class="col-span-12 flex flex-col justify-between bg-white ring ring-gray-200/60 rounded-md md:col-span-4">
3
+ <div>
4
+ <div class="px-4 py-3 bg-gray-50">
5
+ <h3 class="text-sm font-medium tracking-tight text-gray-800 md:text-base">
6
+ <%= plan[:name] %>
7
+ </h3>
8
+
9
+ <p class="mt-1">
10
+ <span class="text-lg font-light text-gray-800">$</span>
11
+ <span class="text-3xl font-semibold text-gray-800"><%= plan[:amount] %></span>
12
+ <span class="text-base font-light text-gray-800">/month</span>
13
+ </p>
14
+ </div>
15
+
16
+ <ul class="grid gap-2 grow-1 px-4 py-4 bg-white border-t border-gray-100">
17
+ <% plan[:features].each do |key, value| %>
18
+ <% next if value == false %>
19
+
20
+ <li class="flex items-center gap-x-1.5 text-sm font-medium text-gray-800">
21
+ <%= icon "check", class: "size-3 text-brand-600 shrink-0" rescue nil %>
22
+
23
+ <% if value == true %>
24
+ <%= key.to_s.humanize %>
25
+ <% else %>
26
+ <% label = key.to_s.gsub('_count', '').humanize.downcase %>
27
+ <%= pluralize(number_with_delimiter(value), label) %>
28
+ <% end %>
29
+ </li>
30
+ <% end %>
31
+ </ul>
32
+ </div>
33
+
34
+ <%= button_to "Subscribe", subscriptions_path, form_class: "flex justify-center px-4 py-3 border-t border-gray-100", params: {price_id: plan[:price_id]}, class: "btn-primary btn-block" %>
35
+ </li>
@@ -0,0 +1,11 @@
1
+ <%= component("heading", breadcrumbs: [{label: "Settings", href: settings_path}]) { "Subscription" } %>
2
+
3
+ <%= component "container" do %>
4
+ <%= tag.ul class: "grid grid-cols-12 gap-4 w-full my-20 md:gap-8" do %>
5
+ <% Config::Plans.except(:default).values.each do |plan| %>
6
+ <%= render "plan", plan: plan %>
7
+ <% end %>
8
+ <% end unless Current.workspace.subscribed? %>
9
+
10
+ <%= button_to "Go to billing", edit_subscriptions_path, method: :get, data: {turbo: false}, class: "btn-secondary" if Current.workspace.subscribed? %>
11
+ <% end %>
@@ -1,6 +1,6 @@
1
- <% content_for :title, "Account" %>
1
+ <% content_for :title, "User settings" %>
2
2
 
3
- <%= component("heading", breadcrumbs: [{label: "Settings", href: settings_path}]) { "Account" } %>
3
+ <%= component("heading", breadcrumbs: [{label: "Settings", href: settings_path}]) { "User" } %>
4
4
 
5
5
  <%= component "container" do %>
6
6
  <%= form_with model: [:settings, @user], class: "max-w-lg" do |form| %>
@@ -1,6 +1,8 @@
1
1
  <% content_for :title, "Create your account" %>
2
2
  <% content_for :page_title, "Create your account" %>
3
3
 
4
+ <%= tag.p "Configure Active Record Encryption before continuing", class: "mx-4 px-2 py-1 text-sm font-medium text-center text-red-900 bg-red-50 border border-red-100 rounded-md" if Rails.env.development? && Rails.application.credentials.active_record_encryption&.primary_key.blank? %>
5
+
4
6
  <%= form_with model: @signup, url: signup_path, class: "px-2 md:px-4" do |form| %>
5
7
  <%= form.input :email_address %>
6
8
 
@@ -9,9 +9,10 @@
9
9
  <%= csp_meta_tag %>
10
10
 
11
11
  <%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
12
+ <script defer src="https://cdn.jsdelivr.net/npm/attractivejs@0.9.0"></script>
12
13
  </head>
13
14
 
14
- <body class="<%= class_names("antialiased bg-white selection:bg-pink-200/40 selection:text-pink-800/70", yield(:body_class)) %>">
15
+ <body class="<%= class_names("antialiased bg-white selection:bg-blue-200/40 selection:text-blue-800/70", yield(:body_class)) %>">
15
16
  <div class="flex">
16
17
  <%= render partial: "layouts/kern/application/navigation",
17
18
  locals: {
@@ -27,5 +28,6 @@
27
28
  </div>
28
29
 
29
30
  <%= component "flash" %>
31
+ <%= component "dialog" %>
30
32
  </body>
31
33
  </html>
@@ -9,6 +9,7 @@
9
9
  <%%= csp_meta_tag %>
10
10
 
11
11
  <%%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
12
+ <script defer src="https://cdn.jsdelivr.net/npm/attractivejs@0.9.0"></script>
12
13
  </head>
13
14
 
14
15
  <body class="<%%= class_names("antialiased bg-white selection:bg-pink-200/40 selection:text-pink-800/70", yield(:body_class)) %>">
@@ -12,7 +12,7 @@
12
12
  <%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
13
13
  </head>
14
14
 
15
- <body class="relative antialiased overflow-clip">
15
+ <body class="relative overflow-clip antialiased bg-white selection:bg-blue-200/40 selection:text-blue-800/70">
16
16
  <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="absolute -top-[400px] -left-[350px] size-[800px] text-brand-500">
17
17
  <path fill="currentColor" d="M42.9,-43.4C60.9,-36,84.8,-27.6,92.2,-12.2C99.8,3.1,91.2,25.4,76.1,37C61.1,48.5,39.7,49.3,21.9,52.7C4,56.1,-10.4,62,-26.6,61.1C-42.9,60.3,-61.1,52.9,-61.9,41C-62.6,28.8,-46,12.1,-42.2,-7C-38.4,-25.9,-47.4,-46.5,-42.5,-56.1C-37.6,-65.8,-18.8,-64.5,-3.2,-60.7C12.4,-57,24.9,-50.8,42.9,-43.4Z" transform="translate(100 100)" />
18
18
  </svg>
@@ -0,0 +1,31 @@
1
+ module Stripe
2
+ class Base < Fuik::Event
3
+ def self.verify!(request)
4
+ secret = Rails.application.credentials.dig(:stripe, :signing_secret)
5
+ signature = request.headers["Stripe-Signature"]
6
+
7
+ Stripe::Webhook.construct_event(
8
+ request.raw_post,
9
+ signature,
10
+ secret
11
+ )
12
+ rescue JSON::ParserError, Stripe::SignatureVerificationError => error
13
+ raise Fuik::InvalidSignature, error.message
14
+ end
15
+
16
+ private
17
+
18
+ def subscription_resource
19
+ @subscription_resource ||= ::Stripe::Subscription.retrieve(
20
+ payload["data"]["object"]["id"],
21
+ expand: ["items.data.price"]
22
+ )
23
+ end
24
+
25
+ def workspace = billing_profile.workspace
26
+
27
+ def billing_profile
28
+ BillingProfile.find_by!(external_id: payload["data"]["object"]["customer"])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ module Stripe
2
+ class CheckoutSessionCompleted < Base
3
+ def process!
4
+ billing_profile = create_billing_profile_for workspace
5
+
6
+ attach_subscription_items_to billing_profile
7
+
8
+ @webhook_event.processed!
9
+ end
10
+
11
+ private
12
+
13
+ def create_billing_profile_for(workspace)
14
+ BillingProfile.where(
15
+ workspace: workspace,
16
+ external_id: payload["data"]["object"]["customer"]
17
+ ).first_or_create
18
+ end
19
+
20
+ def attach_subscription_items_to(billing_profile)
21
+ subscription_resource["items"]["data"].each do |item|
22
+ subscription = BillingProfile::Subscription.find_or_initialize_by(
23
+ billing_profile: billing_profile,
24
+ subscription_item_id: item["id"]
25
+ )
26
+
27
+ subscription.update!(
28
+ subscription_id: subscription_resource["id"],
29
+ status: subscription_resource["status"],
30
+ cancel_at: subscription_resource["cancel_at"] ? Time.at(subscription_resource["cancel_at"]) : nil,
31
+ current_period_end_at: subscription_resource["current_period_end"] ? Time.at(subscription_resource["current_period_end"]) : nil,
32
+ quantity: item["quantity"],
33
+ product_id: item["price"]["product"],
34
+ price_id: item["price"]["id"],
35
+ interval: item["price"]["recurring"]["interval"]
36
+ )
37
+
38
+ workspace.add_access(item["price"]["product"])
39
+ end
40
+ end
41
+
42
+ def workspace
43
+ @workspace ||= Workspace.sluggable.find(payload["data"]["object"]["client_reference_id"])
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module Stripe
2
+ class CustomerSubscriptionDeleted < Base
3
+ def process!
4
+ cancel_subscription_items_for billing_profile
5
+
6
+ @webhook_event.processed!
7
+ end
8
+
9
+ private
10
+
11
+ def cancel_subscription_items_for(billing_profile)
12
+ billing_profile.subscriptions.where(subscription_id: payload["data"]["object"]["id"]).each do |subscription|
13
+ subscription.update!(
14
+ status: "canceled",
15
+ cancel_at: Time.at(payload["data"]["object"]["canceled_at"] || payload["data"]["object"]["ended_at"] || Time.current.to_i)
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ module Stripe
2
+ class CustomerSubscriptionUpdated < Base
3
+ def process!
4
+ update_subscription_items_for billing_profile
5
+
6
+ @webhook_event.processed!
7
+ end
8
+
9
+ private
10
+
11
+ def update_subscription_items_for(billing_profile)
12
+ subscription_resource["items"]["data"].each do |item|
13
+ subscription = BillingProfile::Subscription.find_or_initialize_by(
14
+ billing_profile: billing_profile,
15
+ subscription_item_id: item["id"]
16
+ )
17
+
18
+ subscription.update!(
19
+ subscription_id: subscription_resource["id"],
20
+ status: subscription_resource["status"],
21
+ cancel_at: subscription_resource["cancel_at"] ? Time.at(subscription_resource["cancel_at"]) : nil,
22
+ current_period_end_at: subscription_resource["current_period_end"] ? Time.at(subscription_resource["current_period_end"]) : nil,
23
+ quantity: item["quantity"],
24
+ product_id: item["price"]["product"],
25
+ price_id: item["price"]["id"],
26
+ interval: item["price"]["recurring"]["interval"]
27
+ )
28
+
29
+ workspace.add_access(item["price"]["product"])
30
+ end
31
+ end
32
+ end
33
+ end
data/bin/release CHANGED
@@ -9,9 +9,8 @@ if [ -z "$VERSION" ]; then
9
9
  exit 1
10
10
  fi
11
11
 
12
- # If `VERSION` is a keyword, bump the current version
13
12
  if [[ "$VERSION" =~ ^(major|minor|patch)$ ]]; then
14
- CURRENT=$(grep -oP 'VERSION = "\K[^"]+' ./lib/kern/version.rb)
13
+ CURRENT=$(grep -o '"[^"]*"' ./lib/kern/version.rb | tr -d '"')
15
14
  IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
16
15
 
17
16
  case $VERSION in
@@ -31,5 +30,5 @@ git push
31
30
  git tag v$VERSION
32
31
  git push --tags
33
32
 
34
- rake build
33
+ bundle exec rake build
35
34
  gem push "pkg/kern-$VERSION.gem"
@@ -0,0 +1,5 @@
1
+ if defined?(Stripe)
2
+ Stripe.api_key = Config::Stripe.api_key
3
+ Stripe.api_version = Config::Stripe.api_version
4
+ Stripe.max_network_retries = Config::Stripe.max_network_retries
5
+ end
@@ -0,0 +1,3 @@
1
+ Stripe.api_key = Config::Stripe.api_key
2
+ Stripe.api_version = Config::Stripe.api_version
3
+ Stripe.max_network_retries = Config::Stripe.max_network_retries
data/config/routes.rb CHANGED
@@ -2,8 +2,12 @@ Kern::Engine.routes.draw do
2
2
  resource :settings, only: %w[show]
3
3
  namespace :settings do
4
4
  resource :user, path: "account", only: %w[show update]
5
+
6
+ resource :subscriptions, only: %w[show]
5
7
  end
6
8
 
9
+ resource :subscriptions, module: :billing_profiles, only: %w[create edit]
10
+
7
11
  resource :signup, only: %w[new create]
8
12
 
9
13
  resource :session, only: %w[new create destroy]
@@ -0,0 +1,14 @@
1
+ class CreateBillingProfiles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :billing_profiles do |t|
4
+ t.belongs_to :workspace, null: false, foreign_key: true
5
+ t.string :slug, null: true
6
+ t.string :external_id, null: true # This would be payment provider's (Stripe's) customer_id
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :billing_profiles, [:slug, :workspace_id], unique: true
12
+ add_index :billing_profiles, :external_id
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ class CreateBillingProfileSubscriptions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :billing_profile_subscriptions do |t|
4
+ t.belongs_to :billing_profile, null: false, foreign_key: true
5
+ t.string :slug, null: false
6
+ t.string :subscription_id, null: false
7
+ t.string :subscription_item_id, null: false
8
+ t.string :status, null: false
9
+ t.datetime :current_period_end_at, null: true
10
+ t.datetime :cancel_at, null: true
11
+ t.json :metadata, null: false, default: {}
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :billing_profile_subscriptions, [:slug, :billing_profile_id], unique: true
17
+ add_index :billing_profile_subscriptions, :subscription_id
18
+ add_index :billing_profile_subscriptions, :subscription_item_id
19
+ end
20
+ end
@@ -11,6 +11,7 @@ Example:
11
11
  This will generate sessions and passwords features.
12
12
 
13
13
  Available features:
14
+ billing Billing and subscription functionality
14
15
  passwords Password reset functionality
15
16
  sessions User authentication (sign in/out)
16
17
  settings User settings (user, workspace settings and subscription/billing)
@@ -2,23 +2,27 @@ module Kern
2
2
  class FeatureGenerator < Rails::Generators::Base
3
3
  source_root File.expand_path("../../../../../", __FILE__)
4
4
 
5
- AVAILABLE_FEATURES = %w[passwords sessions settings signups]
5
+ AVAILABLE_FEATURES = %w[billing passwords sessions settings signups]
6
6
 
7
7
  argument :features, type: :array, required: true, banner: "feature feature"
8
8
 
9
+ class_option :skip_encryption, type: :boolean, default: false, desc: "Skip encryption for sensitive fields"
10
+
9
11
  def copy_features
10
12
  features.each do |feature|
11
13
  verify_exists!(feature)
12
14
 
13
15
  case feature
14
- when "sessions"
15
- generate_sessions
16
- when "signups"
17
- generate_signups
16
+ when "billing"
17
+ generate_billing
18
18
  when "passwords"
19
19
  generate_passwords
20
+ when "sessions"
21
+ generate_sessions
20
22
  when "settings"
21
23
  generate_settings
24
+ when "signups"
25
+ generate_signups
22
26
  end
23
27
  end
24
28
  end
@@ -33,39 +37,61 @@ module Kern
33
37
  exit(1)
34
38
  end
35
39
 
36
- def generate_sessions
37
- directory "app/models", "app/models"
38
- template "app/controllers/concerns/authentication.rb.tt", "app/controllers/concerns/authentication.rb"
39
- copy_file "app/controllers/kern/sessions_controller.rb", "app/controllers/sessions_controller.rb"
40
- directory "app/views/kern/sessions", "app/views/sessions"
40
+ def generate_billing
41
+ template "config/initializers/stripe.rb.tt", "config/initializers/stripe.rb"
41
42
 
42
- route "resource :session, only: %w[new create destroy]"
43
- end
43
+ [
44
+ "billing_profile.rb",
45
+ "billing_profile/subscription.rb",
46
+ "workspace/billable.rb"
47
+ ].each { copy_file "app/models/#{it}", "app/models/#{it}" }
44
48
 
45
- def generate_signups
46
- directory "app/models", "app/models" unless features.include?("sessions")
49
+ inject_into_file "app/models/workspace.rb", " include Billable\n\n", after: "include Sluggable\n"
47
50
 
48
- template "app/controllers/kern/signups_controller.rb.tt", "app/controllers/signups_controller.rb"
49
- directory "app/views/kern/signups", "app/views/signups"
51
+ route "resource :subscriptions, module: :billing_profiles, only: %w[create edit]"
52
+ inject_into_file "config/routes.rb", " resource :subscriptions, only: %w[show]\n", after: "namespace :settings do\n"
50
53
 
51
- route "resource :signup, only: %w[new create]"
54
+ copy_file "app/views/kern/settings/subscriptions/show.html.erb", "app/views/settings/subscriptions/show.html.erb"
55
+ copy_file "app/views/kern/settings/subscriptions/_plan.html.erb", "app/views/settings/subscriptions/_plan.html.erb"
56
+
57
+ copy_file "app/controllers/kern/settings/subscriptions_controller.rb", "app/controllers/settings/subscriptions_controller.rb"
58
+ template "app/controllers/kern/billing_profiles/subscriptions_controller.rb.tt", "app/controllers/billing_profiles/subscriptions_controller.rb"
52
59
  end
53
60
 
54
61
  def generate_passwords
55
- directory "app/views/kern/passwords", "app/views/passwords"
56
62
  copy_file "app/controllers/kern/passwords_controller.rb", "app/controllers/passwords_controller.rb"
57
63
  copy_file "app/mailers/kern/passwords_mailer.rb", "app/mailers/passwords_mailer.rb"
64
+
58
65
  template "app/views/kern/passwords_mailer/reset.text.erb.tt", "app/views/passwords_mailer/reset.text.erb"
59
66
  template "app/views/kern/passwords_mailer/reset.html.erb.tt", "app/views/passwords_mailer/reset.html.erb"
60
67
 
68
+ copy_file "app/views/kern/passwords/new.html.erb", "app/views/passwords/new.html.erb"
69
+ copy_file "app/views/kern/passwords/edit.html.erb", "app/views/passwords/edit.html.erb"
70
+
61
71
  route "resources :passwords, param: :token, only: %w[new create edit update]"
62
72
  end
63
73
 
74
+ def generate_sessions
75
+ AUTHENTICATION_MODELS.each { copy_file "app/models/#{it}", "app/models/#{it}" }
76
+
77
+ template "app/controllers/concerns/authentication.rb.tt", "app/controllers/concerns/authentication.rb"
78
+ copy_file "app/controllers/kern/sessions_controller.rb", "app/controllers/sessions_controller.rb"
79
+
80
+ copy_file "app/views/kern/sessions/new.html.erb", "app/views/sessions/new.html.erb"
81
+
82
+ remove_encryption_from_models
83
+
84
+ route "resource :session, only: %w[new create destroy]"
85
+ end
86
+
64
87
  def generate_settings
65
- directory "app/views/kern/settings", "app/views/settings"
66
88
  copy_file "app/controllers/kern/settings_controller.rb", "app/controllers/settings_controller.rb"
67
89
  directory "app/controllers/kern/settings", "app/controllers/settings"
68
90
 
91
+ copy_file "app/views/kern/settings/show.html.erb", "app/views/settings/show.html.erb"
92
+ copy_file "app/views/kern/settings/_cards.html.erb", "app/views/settings/_cards.html.erb"
93
+ copy_file "app/views/kern/settings/users/show.html.erb", "app/views/settings/users/show.html.erb"
94
+
69
95
  route <<~RUBY
70
96
  resource :settings, only: %w[show]
71
97
  namespace :settings do
@@ -73,5 +99,42 @@ module Kern
73
99
  end
74
100
  RUBY
75
101
  end
102
+
103
+ def generate_signups
104
+ AUTHENTICATION_MODELS.each { copy_file "app/models/#{it}", "app/models/#{it}" } unless features.include?("sessions")
105
+
106
+ template "app/controllers/kern/signups_controller.rb.tt", "app/controllers/signups_controller.rb"
107
+
108
+ copy_file "app/views/kern/signups/new.html.erb", "app/views/signups/new.html.erb"
109
+
110
+ remove_encryption_from_models
111
+
112
+ route "resource :signup, only: %w[new create]"
113
+ end
114
+
115
+ def remove_encryption_from_models
116
+ return unless options[:skip_encryption]
117
+
118
+ gsub_file "app/models/session.rb", /\n encrypts :user_agent, :ip\n/, ""
119
+ gsub_file "app/models/user.rb", /\n encrypts :email, deterministic: true, downcase: true\n/, ""
120
+ end
121
+
122
+ AUTHENTICATION_MODELS = [
123
+ "actor.rb",
124
+ "application_form.rb",
125
+ "current.rb",
126
+ "member.rb",
127
+ "member/acting.rb",
128
+ "member/setup.rb",
129
+ "role.rb",
130
+ "session.rb",
131
+ "signup.rb",
132
+ "user.rb",
133
+ "user/workspace_member.rb",
134
+ "workspace.rb",
135
+ "workspace/feature_access.rb",
136
+ "workspace/members.rb",
137
+ "workspace/setup.rb"
138
+ ]
76
139
  end
77
140
  end
@@ -8,6 +8,23 @@ module Kern
8
8
 
9
9
  class_option :skip_migrations, type: :boolean, default: false
10
10
 
11
+ def add_gems
12
+ gem "fuik"
13
+ gem "rails_icons"
14
+ gem "rails_vault"
15
+ gem "stripe"
16
+ end
17
+
18
+ def enable_bcrypt
19
+ if File.read(File.expand_path("Gemfile", destination_root)).include?('gem "bcrypt"')
20
+ uncomment_lines "Gemfile", /gem "bcrypt"/
21
+
22
+ bundle_command("install --quiet")
23
+ else
24
+ bundle_command("add bcrypt", {}, quiet: true)
25
+ end
26
+ end
27
+
11
28
  def copy_migrations
12
29
  return if options[:skip_migrations]
13
30
 
@@ -26,23 +43,42 @@ module Kern
26
43
  end
27
44
  end
28
45
 
29
- def copy_urls_configuration
46
+ def copy_configurations
30
47
  template "configurations/urls.yml", "config/configurations/urls.yml"
48
+ template "configurations/stripe.yml", "config/configurations/stripe.yml"
49
+ template "configurations/plans.yml", "config/configurations/plans.yml"
50
+
31
51
  template "configurations/README.md", "config/configurations/README.md"
32
52
  end
33
53
 
34
- def enable_bcrypt
35
- if File.read(File.expand_path("Gemfile", destination_root)).include?('gem "bcrypt"')
36
- uncomment_lines "Gemfile", /gem "bcrypt"/
54
+ def setup_workspace_access
55
+ Bundler.with_unbundled_env do
56
+ rails_command "generate rails_vault:install"
57
+ end
37
58
 
38
- bundle_command("install --quiet")
39
- else
40
- bundle_command("add bcrypt", {}, quiet: true)
59
+ source_paths.unshift(File.expand_path("../../../..", __dir__))
60
+ template "app/models/workspace/access.rb.tt", "app/models/workspace/access.rb"
61
+ source_paths.shift
62
+ end
63
+
64
+ def install_fuik
65
+ Bundler.with_unbundled_env do
66
+ rails_command "generate fuik:install"
41
67
  end
68
+
69
+ route "mount Fuik::Engine, at: '/'" if Rails.env.test? # ¯\_(ツ)_/¯
70
+
71
+ source_paths.unshift(File.expand_path("../../../..", __dir__))
72
+ directory "app/webhooks/stripe", "app/webhooks/stripe"
73
+ source_paths.shift
42
74
  end
43
75
 
44
76
  def mount_engine
45
77
  route 'mount Kern::Engine => "/"'
46
78
  end
79
+
80
+ def post_install_message
81
+ readme "POST_INSTALL" if behavior == :invoke
82
+ end
47
83
  end
48
84
  end
@@ -0,0 +1,25 @@
1
+
2
+ Kern is ready. One quick setup step.
3
+
4
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5
+
6
+ REQUIRED SETUP
7
+
8
+ Encryption keys
9
+
10
+ Kern uses Active Record Encryption to encrypt sensitive user data
11
+ (email address, session IP and user agent). Run:
12
+
13
+ $ bin/rails db:encryption:init
14
+
15
+ This will output the required encryption keys you can add to your credential
16
+ files (bin/rails credentials:edit --environment=development).
17
+
18
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19
+
20
+ Next steps:
21
+
22
+ 1. Run migrations: bin/rails db:migrate
23
+ 2. Start your server: bin/dev
24
+ 3. Check out the full documentation at https://saas.railsdesigner.com/
25
+