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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +143 -0
- data/Rakefile +14 -0
- data/app/assets/stylesheets/saas_platform/application.css +15 -0
- data/app/assets/stylesheets/saas_platform/application.tailwind.css +25 -0
- data/app/components/saas_platform/button_component.rb +46 -0
- data/app/components/saas_platform/card_component.html.erb +3 -0
- data/app/components/saas_platform/card_component.rb +29 -0
- data/app/components/saas_platform/notification_component.html.erb +27 -0
- data/app/components/saas_platform/notification_component.rb +11 -0
- data/app/components/saas_platform/order_list_component.html.erb +28 -0
- data/app/components/saas_platform/order_list_component.rb +18 -0
- data/app/controllers/saas_platform/application_controller.rb +31 -0
- data/app/controllers/saas_platform/audit_logs_controller.rb +22 -0
- data/app/controllers/saas_platform/dashboard_controller.rb +14 -0
- data/app/controllers/saas_platform/devise/confirmations_controller.rb +7 -0
- data/app/controllers/saas_platform/devise/passwords_controller.rb +7 -0
- data/app/controllers/saas_platform/devise/registrations_controller.rb +7 -0
- data/app/controllers/saas_platform/devise/sessions_controller.rb +7 -0
- data/app/controllers/saas_platform/devise/unlocks_controller.rb +7 -0
- data/app/controllers/saas_platform/search_controller.rb +15 -0
- data/app/helpers/saas_platform/application_helper.rb +4 -0
- data/app/jobs/saas_platform/application_job.rb +4 -0
- data/app/mailers/saas_platform/application_mailer.rb +6 -0
- data/app/models/saas_platform/account.rb +36 -0
- data/app/models/saas_platform/api_key.rb +24 -0
- data/app/models/saas_platform/application_record.rb +5 -0
- data/app/models/saas_platform/notification.rb +19 -0
- data/app/models/saas_platform/order.rb +43 -0
- data/app/models/saas_platform/order_item.rb +18 -0
- data/app/models/saas_platform/product.rb +15 -0
- data/app/models/saas_platform/role.rb +15 -0
- data/app/models/saas_platform/transaction.rb +15 -0
- data/app/models/saas_platform/user.rb +52 -0
- data/app/models/saas_platform/wallet.rb +38 -0
- data/app/models/saas_platform/webhook_endpoint.rb +18 -0
- data/app/models/saas_platform/webhook_event.rb +10 -0
- data/app/policies/saas_platform/application_policy.rb +53 -0
- data/app/services/saas_platform/analytics_service.rb +29 -0
- data/app/services/saas_platform/payment_service.rb +32 -0
- data/app/services/saas_platform/plan_service.rb +45 -0
- data/app/services/saas_platform/search_service.rb +20 -0
- data/app/views/devise/registrations/new.html.erb +59 -0
- data/app/views/devise/saas_platform_users/confirmations/new.html.erb +16 -0
- data/app/views/devise/saas_platform_users/mailer/confirmation_instructions.html.erb +5 -0
- data/app/views/devise/saas_platform_users/mailer/email_changed.html.erb +7 -0
- data/app/views/devise/saas_platform_users/mailer/password_change.html.erb +3 -0
- data/app/views/devise/saas_platform_users/mailer/reset_password_instructions.html.erb +8 -0
- data/app/views/devise/saas_platform_users/mailer/unlock_instructions.html.erb +7 -0
- data/app/views/devise/saas_platform_users/passwords/edit.html.erb +25 -0
- data/app/views/devise/saas_platform_users/passwords/new.html.erb +16 -0
- data/app/views/devise/saas_platform_users/registrations/edit.html.erb +43 -0
- data/app/views/devise/saas_platform_users/registrations/new.html.erb +29 -0
- data/app/views/devise/saas_platform_users/sessions/new.html.erb +26 -0
- data/app/views/devise/saas_platform_users/shared/_error_messages.html.erb +15 -0
- data/app/views/devise/saas_platform_users/shared/_links.html.erb +25 -0
- data/app/views/devise/saas_platform_users/unlocks/new.html.erb +16 -0
- data/app/views/devise/sessions/new.html.erb +47 -0
- data/app/views/layouts/saas_platform/application.html.erb +17 -0
- data/app/views/layouts/saas_platform/auth.html.erb +48 -0
- data/app/views/layouts/saas_platform/dashboard.html.erb +76 -0
- data/app/views/saas_platform/audit_logs/index.html.erb +43 -0
- data/app/views/saas_platform/dashboard/index.html.erb +50 -0
- data/app/views/saas_platform/search/index.html.erb +75 -0
- data/config/initializers/devise.rb +313 -0
- data/config/initializers/money.rb +115 -0
- data/config/initializers/rack_attack.rb +27 -0
- data/config/locales/devise.en.yml +65 -0
- data/config/routes.rb +16 -0
- data/config/tailwind.config.js +45 -0
- data/db/migrate/20260603000000_devise_create_saas_platform_users.rb +53 -0
- data/db/migrate/20260603000001_rolify_create_saas_platform_roles.rb +19 -0
- data/db/migrate/20260603000002_create_saas_platform_accounts.rb +20 -0
- data/db/migrate/20260603000003_create_saas_platform_banking.rb +28 -0
- data/db/migrate/20260603000004_create_saas_platform_ecommerce.rb +37 -0
- data/db/migrate/20260603000005_create_versions.rb +14 -0
- data/db/migrate/20260603000006_create_saas_platform_api_keys.rb +17 -0
- data/db/migrate/20260603000007_create_saas_platform_webhooks.rb +25 -0
- data/db/migrate/20260603000008_create_saas_platform_notifications.rb +14 -0
- data/db/migrate/20260603000009_add_subscription_to_saas_platform_accounts.rb +12 -0
- data/lib/saas_platform/engine.rb +5 -0
- data/lib/saas_platform/version.rb +3 -0
- data/lib/saas_platform.rb +27 -0
- data/lib/tasks/saas_platform_tasks.rake +4 -0
- 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
|
+
[](https://badge.fury.io/rb/saas_platform)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://rubyonrails.org)
|
|
6
|
+
[]()
|
|
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,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,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,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,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,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
|