saas_platform 0.1.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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +143 -0
  4. data/Rakefile +14 -0
  5. data/app/assets/stylesheets/saas_platform/application.css +15 -0
  6. data/app/assets/stylesheets/saas_platform/application.tailwind.css +25 -0
  7. data/app/components/saas_platform/button_component.rb +46 -0
  8. data/app/components/saas_platform/card_component.html.erb +3 -0
  9. data/app/components/saas_platform/card_component.rb +29 -0
  10. data/app/components/saas_platform/notification_component.html.erb +27 -0
  11. data/app/components/saas_platform/notification_component.rb +11 -0
  12. data/app/components/saas_platform/order_list_component.html.erb +28 -0
  13. data/app/components/saas_platform/order_list_component.rb +18 -0
  14. data/app/controllers/saas_platform/application_controller.rb +31 -0
  15. data/app/controllers/saas_platform/audit_logs_controller.rb +22 -0
  16. data/app/controllers/saas_platform/dashboard_controller.rb +14 -0
  17. data/app/controllers/saas_platform/devise/confirmations_controller.rb +7 -0
  18. data/app/controllers/saas_platform/devise/passwords_controller.rb +7 -0
  19. data/app/controllers/saas_platform/devise/registrations_controller.rb +7 -0
  20. data/app/controllers/saas_platform/devise/sessions_controller.rb +7 -0
  21. data/app/controllers/saas_platform/devise/unlocks_controller.rb +7 -0
  22. data/app/controllers/saas_platform/search_controller.rb +15 -0
  23. data/app/helpers/saas_platform/application_helper.rb +4 -0
  24. data/app/jobs/saas_platform/application_job.rb +4 -0
  25. data/app/mailers/saas_platform/application_mailer.rb +6 -0
  26. data/app/models/saas_platform/account.rb +36 -0
  27. data/app/models/saas_platform/api_key.rb +24 -0
  28. data/app/models/saas_platform/application_record.rb +5 -0
  29. data/app/models/saas_platform/notification.rb +19 -0
  30. data/app/models/saas_platform/order.rb +43 -0
  31. data/app/models/saas_platform/order_item.rb +18 -0
  32. data/app/models/saas_platform/product.rb +15 -0
  33. data/app/models/saas_platform/role.rb +15 -0
  34. data/app/models/saas_platform/transaction.rb +15 -0
  35. data/app/models/saas_platform/user.rb +52 -0
  36. data/app/models/saas_platform/wallet.rb +38 -0
  37. data/app/models/saas_platform/webhook_endpoint.rb +18 -0
  38. data/app/models/saas_platform/webhook_event.rb +10 -0
  39. data/app/policies/saas_platform/application_policy.rb +53 -0
  40. data/app/services/saas_platform/analytics_service.rb +29 -0
  41. data/app/services/saas_platform/payment_service.rb +32 -0
  42. data/app/services/saas_platform/plan_service.rb +45 -0
  43. data/app/services/saas_platform/search_service.rb +20 -0
  44. data/app/views/devise/registrations/new.html.erb +59 -0
  45. data/app/views/devise/saas_platform_users/confirmations/new.html.erb +16 -0
  46. data/app/views/devise/saas_platform_users/mailer/confirmation_instructions.html.erb +5 -0
  47. data/app/views/devise/saas_platform_users/mailer/email_changed.html.erb +7 -0
  48. data/app/views/devise/saas_platform_users/mailer/password_change.html.erb +3 -0
  49. data/app/views/devise/saas_platform_users/mailer/reset_password_instructions.html.erb +8 -0
  50. data/app/views/devise/saas_platform_users/mailer/unlock_instructions.html.erb +7 -0
  51. data/app/views/devise/saas_platform_users/passwords/edit.html.erb +25 -0
  52. data/app/views/devise/saas_platform_users/passwords/new.html.erb +16 -0
  53. data/app/views/devise/saas_platform_users/registrations/edit.html.erb +43 -0
  54. data/app/views/devise/saas_platform_users/registrations/new.html.erb +29 -0
  55. data/app/views/devise/saas_platform_users/sessions/new.html.erb +26 -0
  56. data/app/views/devise/saas_platform_users/shared/_error_messages.html.erb +15 -0
  57. data/app/views/devise/saas_platform_users/shared/_links.html.erb +25 -0
  58. data/app/views/devise/saas_platform_users/unlocks/new.html.erb +16 -0
  59. data/app/views/devise/sessions/new.html.erb +47 -0
  60. data/app/views/layouts/saas_platform/application.html.erb +17 -0
  61. data/app/views/layouts/saas_platform/auth.html.erb +48 -0
  62. data/app/views/layouts/saas_platform/dashboard.html.erb +76 -0
  63. data/app/views/saas_platform/audit_logs/index.html.erb +43 -0
  64. data/app/views/saas_platform/dashboard/index.html.erb +50 -0
  65. data/app/views/saas_platform/search/index.html.erb +75 -0
  66. data/config/initializers/devise.rb +313 -0
  67. data/config/initializers/money.rb +115 -0
  68. data/config/initializers/rack_attack.rb +27 -0
  69. data/config/locales/devise.en.yml +65 -0
  70. data/config/routes.rb +16 -0
  71. data/config/tailwind.config.js +45 -0
  72. data/db/migrate/20260603000000_devise_create_saas_platform_users.rb +53 -0
  73. data/db/migrate/20260603000001_rolify_create_saas_platform_roles.rb +19 -0
  74. data/db/migrate/20260603000002_create_saas_platform_accounts.rb +20 -0
  75. data/db/migrate/20260603000003_create_saas_platform_banking.rb +28 -0
  76. data/db/migrate/20260603000004_create_saas_platform_ecommerce.rb +37 -0
  77. data/db/migrate/20260603000005_create_versions.rb +14 -0
  78. data/db/migrate/20260603000006_create_saas_platform_api_keys.rb +17 -0
  79. data/db/migrate/20260603000007_create_saas_platform_webhooks.rb +25 -0
  80. data/db/migrate/20260603000008_create_saas_platform_notifications.rb +14 -0
  81. data/db/migrate/20260603000009_add_subscription_to_saas_platform_accounts.rb +12 -0
  82. data/lib/saas_platform/engine.rb +5 -0
  83. data/lib/saas_platform/version.rb +3 -0
  84. data/lib/saas_platform.rb +27 -0
  85. data/lib/tasks/saas_platform_tasks.rake +4 -0
  86. metadata +562 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e4e2092f5ba398e0ea833920dc041dea5ad5118e5476b99fbb8d4e4372b15d6f
4
+ data.tar.gz: f5676ec92b0d91aef440aa0e59357a1f1fcb61907d625e9ff608c8e106d970c0
5
+ SHA512:
6
+ metadata.gz: d79b115d14fa563b017ce118bc9ff8bfa1c4f2223db60c9fd42699215a09ba6fbf167454a54c67de92d6a3cdd11e2c8efabdffb5efe2d5e8b8b4c4a61af4e8a4
7
+ data.tar.gz: d747ec29ea25717fc6f83c395573f9b61247f412f7a8d8de991ccdd522a3733c5a9b300f62f0df5e9dcc4782870ba1da90f0c2c54c7c149cfcb198d4b39c37f3
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright shiboshreeroy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # SaasPlatform 💎
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/saas_platform.svg)](https://badge.fury.io/rb/saas_platform)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![Rails Version](https://img.shields.io/badge/Rails-8.0+-red.svg)](https://rubyonrails.org)
6
+ [![Security Scan](https://img.shields.io/badge/Security-Brakeman%20Passed-success.svg)]()
7
+
8
+ **SaasPlatform** is an enterprise-grade, mountable Ruby on Rails 8 engine that delivers a complete **SaaS Operating System**. It provides a premium Apple-inspired GUI, robust multi-tenancy, banking logic, ecommerce, and enterprise security out of the box.
9
+
10
+ ---
11
+
12
+ ## 🏗 Installation
13
+
14
+ Add this line to your application's `Gemfile`:
15
+
16
+ ```ruby
17
+ gem 'saas_platform'
18
+ ```
19
+
20
+ ### 1. Initialize Infrastructure
21
+ Run the following commands to install migrations and prepare your database:
22
+
23
+ ```bash
24
+ $ bundle install
25
+ $ bin/rails saas_platform:install:migrations
26
+ $ bin/rails db:migrate
27
+ ```
28
+
29
+ ### 2. Mount the GUI
30
+ Mount the engine in your `config/routes.rb`. This is where your users and admins will access the platform.
31
+
32
+ ```ruby
33
+ Rails.application.routes.draw do
34
+ # You can mount it at any path, e.g., /app, /admin, or /dashboard
35
+ mount SaasPlatform::Engine => "/app"
36
+ end
37
+ ```
38
+
39
+ ### 3. Add Middleware
40
+ Enable rate-limiting security in `config/application.rb`:
41
+
42
+ ```ruby
43
+ config.middleware.use Rack::Attack
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 🖥 Using the GUI (Graphical User Interface)
49
+
50
+ SaasPlatform features a high-end design system inspired by **macOS** and **iOS**. All interfaces are fully responsive and support Hotwire (Turbo/Stimulus) for a "Single Page App" feel.
51
+
52
+ ### 1. Authentication (Glassmorphic Interface)
53
+ Access the login page at `/app/users/sign_in`.
54
+ * **Design**: Features a centered frosted-glass card with native SF-inspired typography.
55
+ * **Flows**: Includes premium layouts for Sign In, Sign Up, Password Reset, and 2FA Verification.
56
+
57
+ ### 2. The Main Dashboard
58
+ The primary landing zone for users (`/app`).
59
+ * **Sidebar**: Persistent navigation with frosted-glass effects. Includes sections for Overview, Orders, Products, and Settings.
60
+ * **Analytics Widgets**: Real-time charts showing Revenue Growth (Line Chart) and Top Products (Bar Chart).
61
+ * **Recent Activity**: Live data tables showing latest transactions and orders with status badges.
62
+
63
+ ### 3. Global Search 🔍
64
+ Located in the top navigation bar.
65
+ * **Functionality**: Instant search across all your data (Users, Products, Orders).
66
+ * **Security**: Automatically filters results based on the current user's Account (Multi-tenant isolation).
67
+
68
+ ### 4. Notification Center 🔔
69
+ The "Bell" icon in the top right tracks all system activity.
70
+ * **Real-time**: Alerts appear instantly via Turbo Streams.
71
+ * **Management**: Users can view recent notifications and mark them as read directly from the dropdown.
72
+
73
+ ### 5. Enterprise Audit Logs 📜
74
+ Available for accounts on the **Enterprise Plan**.
75
+ * **Location**: `/app/audit_logs`.
76
+ * **GUI Feature**: A detailed list showing exactly who changed what data, including "before and after" snapshots.
77
+
78
+ ---
79
+
80
+ ## 🛠 Developer Guide (Customizing the GUI)
81
+
82
+ ### Using ViewComponents
83
+ You can use the platform's premium UI components in your own host application views:
84
+
85
+ ```erb
86
+ <%# Render a native-feeling card %>
87
+ <%= render SaasPlatform::CardComponent.new(glass: true) do %>
88
+ <h3 class="text-lg font-bold">Custom Widget</h3>
89
+ <p>Your custom content here...</p>
90
+ <% end %>
91
+
92
+ <%# Render a native Apple-style button %>
93
+ <%= render SaasPlatform::ButtonComponent.new(variant: :primary) do %>
94
+ Action Item
95
+ <% end %>
96
+ ```
97
+
98
+ ### Role-Based Access (RBAC)
99
+ Control what users see in the GUI based on their roles:
100
+
101
+ ```ruby
102
+ user = SaasPlatform::User.find(1)
103
+ user.add_role(:admin) # Now has access to admin-only GUI features
104
+ ```
105
+
106
+ ### Plan-Based Gating
107
+ Hide or show GUI features based on the account's subscription:
108
+
109
+ ```ruby
110
+ <% if current_account.can_use?(:advanced_analytics) %>
111
+ <%= render "premium_gui_widget" %>
112
+ <% end %>
113
+ ```
114
+
115
+ ---
116
+
117
+ ## 💰 Banking & Fintech Logic
118
+
119
+ The GUI interfaces directly with a robust backend banking system:
120
+ * **Atomic Wallets**: Every account has a balance managed in cents (`money-rails`).
121
+ * **Transaction Integrity**: Every credit/debit is logged with a polymorphic reference.
122
+
123
+ ```ruby
124
+ # Example: Process a GUI-triggered refund
125
+ current_account.wallet.credit!(order.total, category: "refund", reference: order)
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 🔒 Security Standards
131
+
132
+ * **Rate Limiting**: Throttles brute-force attempts on the GUI login.
133
+ * **Secure Headers**: Injected CSP to prevent XSS and clickjacking.
134
+ * **Multi-Tenancy**: The GUI *never* shows data from another account.
135
+
136
+ ---
137
+
138
+ ## 📄 License
139
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
140
+
141
+ ---
142
+
143
+ Built for excellence by **Shiboshree Roy**. 🚀
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/setup"
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
8
+
9
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
10
+ load "rails/tasks/engine.rake"
11
+ load "rails/tasks/statistics.rake"
12
+
13
+ require "rubocop/rake_task"
14
+ RuboCop::RakeTask.new
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,25 @@
1
+ @import "tailwindcss/base";
2
+ @import "tailwindcss/components";
3
+ @import "tailwindcss/utilities";
4
+
5
+ @layer components {
6
+ .apple-glass {
7
+ @apply bg-white/70 backdrop-blur-apple border border-white/20 shadow-apple;
8
+ }
9
+
10
+ .apple-glass-dark {
11
+ @apply bg-apple-dark/70 backdrop-blur-apple border border-white/10 shadow-apple;
12
+ }
13
+
14
+ .btn-apple {
15
+ @apply inline-flex items-center justify-center px-4 py-2 rounded-apple font-medium transition-all duration-200 active:scale-95 disabled:opacity-50 disabled:pointer-events-none;
16
+ }
17
+
18
+ .btn-apple-primary {
19
+ @apply btn-apple bg-apple-blue text-white hover:bg-opacity-90;
20
+ }
21
+
22
+ .btn-apple-secondary {
23
+ @apply btn-apple bg-apple-light text-apple-dark hover:bg-gray-200;
24
+ }
25
+ }
@@ -0,0 +1,46 @@
1
+ module SaasPlatform
2
+ class ButtonComponent < ViewComponent::Base
3
+ def initialize(variant: :primary, size: :md, type: :button, **system_arguments)
4
+ @variant = variant
5
+ @size = size
6
+ @type = type
7
+ @system_arguments = system_arguments
8
+ end
9
+
10
+ def call
11
+ content_tag(:button, content, type: @type, class: classes, **@system_arguments)
12
+ end
13
+
14
+ private
15
+
16
+ def classes
17
+ [
18
+ base_classes,
19
+ variant_classes,
20
+ size_classes,
21
+ @system_arguments[:class]
22
+ ].compact.join(" ")
23
+ end
24
+
25
+ def base_classes
26
+ "btn-apple"
27
+ end
28
+
29
+ def variant_classes
30
+ case @variant
31
+ when :primary then "btn-apple-primary"
32
+ when :secondary then "btn-apple-secondary"
33
+ when :danger then "bg-apple-red text-white hover:bg-opacity-90"
34
+ when :outline then "border-2 border-apple-blue text-apple-blue hover:bg-apple-blue hover:text-white"
35
+ end
36
+ end
37
+
38
+ def size_classes
39
+ case @size
40
+ when :sm then "text-sm px-3 py-1.5"
41
+ when :md then "text-base px-4 py-2"
42
+ when :lg then "text-lg px-6 py-3"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ <%= content_tag :div, class: classes, **@system_arguments.except(:class) do %>
2
+ <%= content %>
3
+ <% end %>
@@ -0,0 +1,29 @@
1
+ module SaasPlatform
2
+ class CardComponent < ViewComponent::Base
3
+ def initialize(glass: true, padding: :md, **system_arguments)
4
+ @glass = glass
5
+ @padding = padding
6
+ @system_arguments = system_arguments
7
+ end
8
+
9
+ private
10
+
11
+ def classes
12
+ [
13
+ "rounded-apple-lg overflow-hidden",
14
+ @glass ? "apple-glass" : "bg-white border border-gray-200 shadow-sm",
15
+ padding_classes,
16
+ @system_arguments[:class]
17
+ ].compact.join(" ")
18
+ end
19
+
20
+ def padding_classes
21
+ case @padding
22
+ when :none then ""
23
+ when :sm then "p-4"
24
+ when :md then "p-6"
25
+ when :lg then "p-8"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ <div class="relative inline-block text-left" data-controller="dropdown">
2
+ <button type="button" class="relative p-1 text-apple-gray hover:text-apple-blue focus:outline-none" id="notifications-menu" aria-expanded="false" aria-haspopup="true">
3
+ <span class="sr-only">View notifications</span>
4
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
5
+ <% if @notifications.unread.any? %>
6
+ <span class="absolute top-0 right-0 block w-2.5 h-2.5 rounded-full bg-apple-red ring-2 ring-white"></span>
7
+ <% end %>
8
+ </button>
9
+
10
+ <div class="absolute right-0 w-80 mt-2 origin-top-right apple-glass rounded-apple-lg shadow-apple ring-1 ring-black ring-opacity-5 focus:outline-none z-50 overflow-hidden" role="menu" aria-orientation="vertical" aria-labelledby="notifications-menu" tabindex="-1">
11
+ <div class="px-4 py-3 border-b border-gray-100 flex justify-between items-center">
12
+ <h3 class="text-sm font-semibold text-apple-dark">Notifications</h3>
13
+ <a href="#" class="text-xs text-apple-blue hover:underline">Mark all as read</a>
14
+ </div>
15
+ <div class="max-h-96 overflow-y-auto">
16
+ <% @notifications.recent.limit(5).each do |notification| %>
17
+ <div class="px-4 py-3 hover:bg-apple-light/30 transition-colors border-b border-gray-50 <%= 'bg-blue-50/20' unless notification.read? %>">
18
+ <p class="text-sm text-apple-dark"><%= notification.params['message'] %></p>
19
+ <p class="mt-1 text-xs text-apple-gray"><%= time_ago_in_words(notification.created_at) %> ago</p>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+ <div class="px-4 py-2 bg-apple-light/50 text-center">
24
+ <a href="#" class="text-xs font-medium text-apple-blue hover:underline">View all notifications</a>
25
+ </div>
26
+ </div>
27
+ </div>
@@ -0,0 +1,11 @@
1
+ module SaasPlatform
2
+ class NotificationComponent < ViewComponent::Base
3
+ def initialize(notifications:)
4
+ @notifications = notifications
5
+ end
6
+
7
+ def render?
8
+ @notifications.any?
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ <div class="overflow-x-auto">
2
+ <table class="min-w-full divide-y divide-gray-200">
3
+ <thead>
4
+ <tr>
5
+ <th class="px-6 py-3 text-left text-xs font-medium text-apple-gray uppercase tracking-wider">Order ID</th>
6
+ <th class="px-6 py-3 text-left text-xs font-medium text-apple-gray uppercase tracking-wider">Customer</th>
7
+ <th class="px-6 py-3 text-left text-xs font-medium text-apple-gray uppercase tracking-wider">Status</th>
8
+ <th class="px-6 py-3 text-left text-xs font-medium text-apple-gray uppercase tracking-wider">Total</th>
9
+ <th class="px-6 py-3 text-left text-xs font-medium text-apple-gray uppercase tracking-wider">Date</th>
10
+ </tr>
11
+ </thead>
12
+ <tbody class="divide-y divide-gray-200">
13
+ <% @orders.each do |order| %>
14
+ <tr class="hover:bg-apple-light/30 transition-colors">
15
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-apple-blue">#<%= order.id %></td>
16
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-apple-dark"><%= order.user.name %></td>
17
+ <td class="px-6 py-4 whitespace-nowrap">
18
+ <span class="px-2.5 py-0.5 rounded-full text-xs font-medium <%= status_classes(order.status) %>">
19
+ <%= order.status.humanize %>
20
+ </span>
21
+ </td>
22
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-apple-dark"><%= order.total.format %></td>
23
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-apple-gray"><%= order.created_at.strftime("%b %d, %Y") %></td>
24
+ </tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
28
+ </div>
@@ -0,0 +1,18 @@
1
+ module SaasPlatform
2
+ class OrderListComponent < ViewComponent::Base
3
+ def initialize(orders:)
4
+ @orders = orders
5
+ end
6
+
7
+ private
8
+
9
+ def status_classes(status)
10
+ case status.to_sym
11
+ when :paid, :completed then "bg-apple-green/10 text-apple-green"
12
+ when :pending then "bg-apple-orange/10 text-apple-orange"
13
+ when :cancelled, :refunded then "bg-apple-red/10 text-apple-red"
14
+ else "bg-apple-gray/10 text-apple-gray"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ module SaasPlatform
2
+ class ApplicationController < ActionController::Base
3
+ set_current_tenant_through_filter
4
+ before_action :set_tenant
5
+
6
+ include Pundit::Authorization
7
+
8
+ helper_method :current_account
9
+
10
+ rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
11
+
12
+ def current_account
13
+ current_tenant
14
+ end
15
+
16
+ private
17
+
18
+ def set_tenant
19
+ # For simplicity, we'll use the current user's account if they are logged in.
20
+ # In a real SaaS, this would be based on subdomain or domain.
21
+ if current_user
22
+ set_current_tenant(current_user.account)
23
+ end
24
+ end
25
+
26
+ def user_not_authorized
27
+ flash[:alert] = "You are not authorized to perform this action."
28
+ redirect_to(request.referrer || main_app.root_path)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ module SaasPlatform
2
+ class AuditLogsController < ApplicationController
3
+ before_action :authenticate_user!
4
+ before_action :check_enterprise_access
5
+ layout 'saas_platform/dashboard'
6
+
7
+ def index
8
+ @versions = PaperTrail::Version.where(item_type: ['SaasPlatform::User', 'SaasPlatform::Wallet', 'SaasPlatform::Order'])
9
+ .order(created_at: :desc)
10
+ .page(params[:page])
11
+ end
12
+
13
+ private
14
+
15
+ def check_enterprise_access
16
+ unless current_account.can_use?(:audit_logs)
17
+ flash[:alert] = "Audit logs are only available on Enterprise plans."
18
+ redirect_to root_path
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module SaasPlatform
2
+ class DashboardController < ApplicationController
3
+ before_action :authenticate_user!
4
+ layout 'saas_platform/dashboard'
5
+
6
+ def index
7
+ @analytics = AnalyticsService.new(current_account)
8
+ @revenue_data = @analytics.revenue_over_time
9
+ @top_products = @analytics.top_products
10
+ @user_growth = @analytics.user_growth
11
+ @recent_orders = current_account.orders.includes(:user).order(created_at: :desc).limit(10)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module SaasPlatform
2
+ module Devise
3
+ class ConfirmationsController < ::Devise::ConfirmationsController
4
+ layout 'saas_platform/auth'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module SaasPlatform
2
+ module Devise
3
+ class PasswordsController < ::Devise::PasswordsController
4
+ layout 'saas_platform/auth'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module SaasPlatform
2
+ module Devise
3
+ class RegistrationsController < ::Devise::RegistrationsController
4
+ layout 'saas_platform/auth'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module SaasPlatform
2
+ module Devise
3
+ class SessionsController < ::Devise::SessionsController
4
+ layout 'saas_platform/auth'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module SaasPlatform
2
+ module Devise
3
+ class UnlocksController < ::Devise::UnlocksController
4
+ layout 'saas_platform/auth'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module SaasPlatform
2
+ class SearchController < ApplicationController
3
+ before_action :authenticate_user!
4
+
5
+ def index
6
+ @query = params[:q]
7
+ @results = SearchService.new(current_account, @query).perform if @query.present?
8
+
9
+ respond_to do |format|
10
+ format.html
11
+ format.turbo_stream
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module SaasPlatform
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module SaasPlatform
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module SaasPlatform
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,36 @@
1
+ module SaasPlatform
2
+ class Account < ApplicationRecord
3
+ include PgSearch::Model
4
+ pg_search_scope :search_by_name, against: [:name, :subdomain, :domain]
5
+
6
+ has_many :users, class_name: 'SaasPlatform::User', dependent: :destroy
7
+ has_one :wallet, class_name: 'SaasPlatform::Wallet', dependent: :destroy
8
+ has_many :products, class_name: 'SaasPlatform::Product', dependent: :destroy
9
+ has_many :orders, class_name: 'SaasPlatform::Order', dependent: :destroy
10
+
11
+ after_create :create_default_wallet
12
+
13
+ validates :name, presence: true
14
+ validates :subdomain, presence: true, uniqueness: true, format: { with: /\A[a-z0-9-]+\z/ }
15
+
16
+ scope :active, -> { where(status: 'active') }
17
+
18
+ def self.current
19
+ ActsAsTenant.current_tenant
20
+ end
21
+
22
+ def plan
23
+ @plan ||= PlanService.new(self)
24
+ end
25
+
26
+ def can_use?(feature)
27
+ plan.has_feature?(feature)
28
+ end
29
+
30
+ private
31
+
32
+ def create_default_wallet
33
+ create_wallet(currency: 'USD')
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ module SaasPlatform
2
+ class ApiKey < ApplicationRecord
3
+ acts_as_tenant :account, class_name: 'SaasPlatform::Account'
4
+
5
+ belongs_to :user, class_name: 'SaasPlatform::User'
6
+
7
+ validates :access_token, presence: true, uniqueness: true
8
+ validates :name, presence: true
9
+
10
+ before_validation :generate_access_token, on: :create
11
+
12
+ scope :active, -> { where(active: true).where('expires_at IS NULL OR expires_at > ?', Time.current) }
13
+
14
+ def self.authenticate(token)
15
+ active.find_by(access_token: token)
16
+ end
17
+
18
+ private
19
+
20
+ def generate_access_token
21
+ self.access_token = "sp_#{SecureRandom.hex(24)}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module SaasPlatform
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ module SaasPlatform
2
+ class Notification < ApplicationRecord
3
+ acts_as_tenant :account, class_name: 'SaasPlatform::Account'
4
+
5
+ belongs_to :recipient, polymorphic: true
6
+
7
+ scope :unread, -> { where(read_at: nil) }
8
+ scope :read, -> { where.not(read_at: nil) }
9
+ scope :recent, -> { order(created_at: :desc) }
10
+
11
+ def read?
12
+ read_at.present?
13
+ end
14
+
15
+ def mark_as_read!
16
+ update!(read_at: Time.current)
17
+ end
18
+ end
19
+ end