one-for-all-framework 4.4.0 → 4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 345583a2af3a1c7ee4eb6d923c1d87ea769a5f6e70c98537a36575d1ee89dc03
4
- data.tar.gz: 596e7f2abe8bcf277d180e5044e9b5fb77fbfa894c894c92639d7cda3fc4136a
3
+ metadata.gz: d724d07382e25a25fc566c6c889497bcabca8ef5536f86c13d9337f48507ef74
4
+ data.tar.gz: d7322c62e8380e22f17773b3202a7574859db716289706591db041205df38b61
5
5
  SHA512:
6
- metadata.gz: 9b5c07c96b28fc73548e35ecbb81113361d6b32575c4a681bf7bf0b916e2c39e4b18e34032f252c83176116a89d4377d8f1f58ee16acab90cdafb4d0ce467b03
7
- data.tar.gz: bcb67f5526ebca56809ec9fa8009199b0911c2668445b8eb15ecd7f5995edde4fd8b33271b4168a416fc04aa87f768947d8109a1e0816a97b2620571cd1f911d
6
+ metadata.gz: 896e3107819354a1e335519e4a4ec8f6934bdab41ea7f2e99558bed67ede658295b1be2aeead6d50608d5a9c3b51603e71d1754837f6c45cd93b43580a5b1a72
7
+ data.tar.gz: 7d9f02100feb5a46ee909932672c03443e585ba7fd6641d05d84006f3de780f5de85e56e381a84c0b4337fc6c59a5e9ea621f5281c112f8093be3063d19c3360
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <img src="public/images/logo.png" width="500" height="500" alt="OFA Framework Logo">
3
3
  </p>
4
4
 
5
- # ⚡ One-For-All (OFA) Framework v4.4.0
5
+ # ⚡ One-For-All (OFA) Framework v4.5.0
6
6
 
7
7
  [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.0.0-red.svg)](https://www.ruby-lang.org/)
8
8
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -22,6 +22,9 @@
22
22
  - **🌐 Global Support**: Multi-language (I18n) support and SEO optimization ready.
23
23
  - **🖋️ Rich Text Editor**: Integrated Trix Editor for seamless post and page creation with image upload support.
24
24
  - **📡 Modern API**: Built-in JWT support and automated Swagger/OpenAPI documentation.
25
+ - **🚧 Maintenance Mode**: Instantly toggle "Under Construction" mode via CLI.
26
+ - **🔍 Activity Logs**: Full audit trail in CMS to monitor user actions and security.
27
+ - **🔌 Plugin System**: Modular architecture to extend the framework with custom add-ons.
25
28
 
26
29
  ---
27
30
 
@@ -129,6 +132,10 @@ Switches between local disk and Cloudinary cloud storage.
129
132
  Securely manages admin credentials.
130
133
  * **Output:** `✅ Password for 'admin' updated successfully.`
131
134
 
135
+ #### `ofa maintenance [on|off]`
136
+ Instantly toggles a premium "Under Construction" page for visitors while allowing admin access.
137
+ * **Output:** `✅ Maintenance mode has been turned on.`
138
+
132
139
  ---
133
140
 
134
141
  ### 📁 Project Lifecycle
@@ -143,6 +150,7 @@ Securely manages admin credentials.
143
150
  | `ofa swagger` | **OpenAPI Generation.** Auto-generates `openapi.json` for your entire application. |
144
151
  | `ofa task NAME` | **Run Background Task.** Executes a task defined in `lib/tasks/`. |
145
152
  | `ofa test` | **Run Test Suite.** Executes all unit tests in the `test/` directory using Minitest. |
153
+ | `ofa maintenance on/off` | **Toggle Site Access.** Activates a beautiful maintenance page. Admins can still bypass it to work. |
146
154
  | `ofa deploy` | **Production Deployment.** Automatically detects deployment targets (Railway/Docker/Git). |
147
155
 
148
156
  ---
@@ -159,6 +167,7 @@ Automate the creation of boilerplate code with the generator command.
159
167
  | `ofa g mailer NAME ACTION` | Generates a new mailer in `app/mailers/` and an ERB template in `app/views/mailers/`. |
160
168
  | `ofa g task NAME` | Creates a new background task file in `lib/tasks/`. |
161
169
  | `ofa g test NAME` | Generates a new Minitest unit test in `test/`. |
170
+ | `ofa g plugin NAME` | **Plugin Scaffolding.** Creates a new plugin structure in the `plugins/` directory. |
162
171
  | `ofa g post TITLE` | Creates a new Markdown/ERB post in `app/views/posts/`. <br> *Args:* `--category`, `--author`, `--image`. <br> *Example:* `./ofa g post "My First Journey" --category Tech --author "John Doe"` |
163
172
 
164
173
  ---
@@ -0,0 +1,9 @@
1
+ require_relative 'application_controller'
2
+
3
+ class ActivityLogsController < ApplicationController
4
+ def index
5
+ # Fetch logs with user association
6
+ @logs = ActivityLog.order(Sequel.desc(:created_at)).limit(100).all
7
+ render 'cms/activity_logs'
8
+ end
9
+ end
@@ -46,6 +46,15 @@ class ApplicationController
46
46
  @req.env['eks_cent.csrf_token']
47
47
  end
48
48
 
49
+ # Activity Logger
50
+ def log_activity(action, target = nil, details = nil)
51
+ user_id = session['user_id'] || session[:user_id]
52
+ return unless user_id
53
+ ActivityLog.log(user_id, action, target, details)
54
+ end
55
+
56
+ private
57
+
49
58
  def boolean_param(val)
50
59
  ['1', 'true', 'on'].include?(val.to_s)
51
60
  end
@@ -17,6 +17,7 @@ class PagesController < ApplicationController
17
17
  data['is_nav'] = boolean_param(data['is_nav'])
18
18
  @page = Page.new(data)
19
19
  if @page.save
20
+ log_activity("Created page: #{@page.title}", @page, "Slug: #{@page.slug}, Nav: #{@page.is_nav}")
20
21
  redirect_to '/dashboard/pages'
21
22
  else
22
23
  render 'cms/pages_form'
@@ -34,6 +35,7 @@ class PagesController < ApplicationController
34
35
  data['is_active'] = boolean_param(data['is_active'])
35
36
  data['is_nav'] = boolean_param(data['is_nav'])
36
37
  if @page.update(data)
38
+ log_activity("Updated page: #{@page.title}", @page, "New Data: #{data.reject{|k| k == 'content'}.to_json}")
37
39
  redirect_to '/dashboard/pages'
38
40
  else
39
41
  render 'cms/pages_form'
@@ -42,8 +44,10 @@ class PagesController < ApplicationController
42
44
 
43
45
  def destroy
44
46
  @page = Page[params['id']]
47
+ title = @page.title
45
48
  delete_all_images_from_content(@page.content) if @page.respond_to?(:content)
46
49
  @page.destroy
50
+ log_activity("Deleted page: #{title}")
47
51
  redirect_to '/dashboard/pages'
48
52
  end
49
53
  end
@@ -16,6 +16,7 @@ class PostsController < ApplicationController
16
16
  data['is_active'] = boolean_param(data['is_active'])
17
17
  @post = Post.new(data)
18
18
  if @post.save
19
+ log_activity("Created post: #{@post.title}", @post, "Category: #{@post.category}")
19
20
  redirect_to '/dashboard/posts'
20
21
  else
21
22
  render 'cms/posts_form'
@@ -32,6 +33,7 @@ class PostsController < ApplicationController
32
33
  data = params['post']
33
34
  data['is_active'] = boolean_param(data['is_active'])
34
35
  if @post.update(data)
36
+ log_activity("Updated post: #{@post.title}", @post, "New Data: #{data.reject{|k| k == 'content'}.to_json}")
35
37
  redirect_to '/dashboard/posts'
36
38
  else
37
39
  render 'cms/posts_form'
@@ -40,9 +42,11 @@ class PostsController < ApplicationController
40
42
 
41
43
  def destroy
42
44
  @post = Post[params['id']]
45
+ title = @post.title
43
46
  delete_from_storage(@post.image_url) if @post.respond_to?(:image_url)
44
47
  delete_all_images_from_content(@post.content) if @post.respond_to?(:content)
45
48
  @post.destroy
49
+ log_activity("Deleted post: #{title}")
46
50
  redirect_to '/dashboard/posts'
47
51
  end
48
52
  end
@@ -0,0 +1,32 @@
1
+ class MaintenanceMiddleware
2
+ def initialize(app)
3
+ @app = app
4
+ end
5
+
6
+ def call(env)
7
+ # Reload config to ensure instant mode change without server restart
8
+ config = JSON.parse(File.read(File.join(APP_ROOT, 'config', 'features.json'))) rescue (defined?(FEATURES_CONFIG) ? FEATURES_CONFIG : {})
9
+
10
+ if config['maintenance']
11
+ path = env['PATH_INFO']
12
+
13
+ # Allow access to login, static assets, and CMS dashboard (if admin)
14
+ # This allows admins to turn off maintenance mode via the dashboard
15
+ allowed_paths = ['/login', '/logout', '/images', '/css', '/js', '/favicon.ico']
16
+ is_allowed = allowed_paths.any? { |p| path.start_with?(p) }
17
+
18
+ # Also allow if already logged in (admin access)
19
+ session = env['eks_cent.session'] || env['rack.session'] || {}
20
+ is_admin = session['user_id'] || session[:user_id]
21
+
22
+ if !is_allowed && !is_admin
23
+ # Render maintenance page
24
+ res = EksCent::Response.new
25
+ res.render('maintenance', layout: false)
26
+ return [503, res.headers, res.body]
27
+ end
28
+ end
29
+
30
+ @app.call(env)
31
+ end
32
+ end
@@ -0,0 +1,26 @@
1
+ class ActivityLog < Sequel::Model
2
+ # Custom user lookup to support both SQL and Mongo users
3
+ def user
4
+ return @user if defined?(@user)
5
+ @user = User.find(id: self.user_id) rescue nil
6
+ end
7
+
8
+ def self.log(user_id, action, target = nil, details = nil)
9
+ log_entry = {
10
+ user_id: user_id.to_s,
11
+ action: action,
12
+ created_at: Time.now
13
+ }
14
+
15
+ if target
16
+ log_entry[:target_type] = target.class.name
17
+ log_entry[:target_id] = target.id.to_s rescue nil
18
+ end
19
+
20
+ if details
21
+ log_entry[:details] = details.is_a?(Hash) ? details.to_json : details.to_s
22
+ end
23
+
24
+ create(log_entry)
25
+ end
26
+ end
data/app/models/user.rb CHANGED
@@ -18,6 +18,11 @@ class User < Sequel::Model
18
18
  # Override find for Mongo support
19
19
  def self.find(params)
20
20
  if mongo?
21
+ # Map :id to :_id and handle ObjectId conversion
22
+ if params[:id]
23
+ id_val = params.delete(:id)
24
+ params[:_id] = BSON::ObjectId.from_string(id_val) rescue id_val
25
+ end
21
26
  doc = MONGO_CLIENT[:users].find(params).first
22
27
  return nil unless doc
23
28
  user = User.new
@@ -0,0 +1,177 @@
1
+ <div class="glass-card">
2
+ <div class="card-header flex justify-between items-center">
3
+ <div>
4
+ <h2 class="card-title">Activity Logs (Audit Trail)</h2>
5
+ <p class="card-subtitle">Keep track of who did what in the CMS dashboard.</p>
6
+ </div>
7
+ <a href="/dashboard" class="btn-premium !py-2 !px-4 text-sm flex items-center gap-2">
8
+ <i class="fas fa-arrow-left"></i> Back to Dashboard
9
+ </a>
10
+ </div>
11
+
12
+ <div class="table-container">
13
+ <table class="premium-table">
14
+ <thead>
15
+ <tr>
16
+ <th>Time</th>
17
+ <th>User</th>
18
+ <th>Action</th>
19
+ <th>Target</th>
20
+ <th>Details</th>
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ <% if @logs.any? %>
25
+ <% @logs.each do |log| %>
26
+ <tr>
27
+ <td data-label="Time" class="text-sm opacity-70">
28
+ <%= log.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
29
+ </td>
30
+ <td data-label="User">
31
+ <div class="flex items-center gap-2">
32
+ <div class="avatar-mini">
33
+ <%= log.user ? log.user.username[0].upcase : '?' %>
34
+ </div>
35
+ <span><%= log.user ? log.user.username : 'Unknown' %></span>
36
+ </div>
37
+ </td>
38
+ <td data-label="Action">
39
+ <span class="badge badge-primary">
40
+ <%= log.action %>
41
+ </span>
42
+ </td>
43
+ <td data-label="Target">
44
+ <% if log.target_type %>
45
+ <span class="text-xs opacity-60"><%= log.target_type %></span>
46
+ <code class="text-xs">#<%= log.target_id %></code>
47
+ <% else %>
48
+ -
49
+ <% end %>
50
+ </td>
51
+ <td data-label="Details" class="text-xs opacity-80 max-w-xs truncate">
52
+ <%= log.details || '-' %>
53
+ </td>
54
+ </tr>
55
+ <% end %>
56
+ <% else %>
57
+ <tr>
58
+ <td colspan="5" class="text-center py-8 opacity-50">
59
+ No logs found yet.
60
+ </td>
61
+ </tr>
62
+ <% end %>
63
+ </tbody>
64
+ </table>
65
+ </div>
66
+ </div>
67
+
68
+ <style>
69
+ .avatar-mini {
70
+ width: 24px;
71
+ height: 24px;
72
+ background: var(--primary);
73
+ color: white;
74
+ border-radius: 50%;
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ font-size: 0.7rem;
79
+ font-weight: 600;
80
+ }
81
+
82
+ .premium-table {
83
+ width: 100%;
84
+ border-collapse: collapse;
85
+ }
86
+
87
+ .premium-table th {
88
+ text-align: left;
89
+ padding: 1rem;
90
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
91
+ font-size: 0.85rem;
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.05em;
94
+ opacity: 0.7;
95
+ }
96
+
97
+ .premium-table td {
98
+ padding: 1rem;
99
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
100
+ font-size: 0.9rem;
101
+ }
102
+
103
+ .premium-table tr:hover {
104
+ background: rgba(255, 255, 255, 0.02);
105
+ }
106
+
107
+ .badge {
108
+ padding: 0.2rem 0.6rem;
109
+ border-radius: 99px;
110
+ font-size: 0.75rem;
111
+ font-weight: 500;
112
+ }
113
+
114
+ .badge-primary {
115
+ background: rgba(99, 102, 241, 0.2);
116
+ color: #818cf8;
117
+ border: 1px solid rgba(99, 102, 241, 0.3);
118
+ }
119
+
120
+ .table-container {
121
+ overflow-x: auto;
122
+ }
123
+
124
+ /* Responsive adjustments */
125
+ @media (max-width: 768px) {
126
+ .card-header {
127
+ flex-direction: column;
128
+ align-items: flex-start !important;
129
+ gap: 1rem;
130
+ }
131
+
132
+ .premium-table thead {
133
+ display: none;
134
+ }
135
+
136
+ .premium-table tr {
137
+ display: block;
138
+ margin-bottom: 1.5rem;
139
+ background: rgba(255, 255, 255, 0.03);
140
+ border-radius: 12px;
141
+ padding: 0.5rem;
142
+ border: 1px solid rgba(255, 255, 255, 0.05);
143
+ }
144
+
145
+ .premium-table td {
146
+ display: flex;
147
+ justify-content: space-between;
148
+ align-items: center;
149
+ padding: 0.75rem 1rem;
150
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
151
+ text-align: right;
152
+ }
153
+
154
+ .premium-table td:last-child {
155
+ border-bottom: none;
156
+ }
157
+
158
+ .premium-table td::before {
159
+ content: attr(data-label);
160
+ font-weight: 600;
161
+ font-size: 0.75rem;
162
+ text-transform: uppercase;
163
+ opacity: 0.5;
164
+ text-align: left;
165
+ margin-right: 1rem;
166
+ }
167
+
168
+ .premium-table td .flex {
169
+ justify-content: flex-end;
170
+ }
171
+
172
+ .truncate {
173
+ white-space: normal;
174
+ max-width: 60%;
175
+ }
176
+ }
177
+ </style>
@@ -35,6 +35,11 @@
35
35
  <span class="font-semibold">Products</span>
36
36
  </a>
37
37
  <% end %>
38
+
39
+ <a href="/dashboard/logs" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/logs') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
40
+ <i class="fas fa-history w-5"></i>
41
+ <span class="font-semibold">Activity Logs</span>
42
+ </a>
38
43
 
39
44
  <div class="h-px bg-white/5 my-4"></div>
40
45
 
data/app/views/docs.erb CHANGED
@@ -1,6 +1,6 @@
1
1
  <div class="space-y-12 pb-20">
2
2
  <div class="text-left">
3
- <div class="badge-premium">Documentation v4.4.0</div>
3
+ <div class="badge-premium">Documentation v4.5.0</div>
4
4
  <h1 class="text-5xl font-black tracking-tighter mb-4 text-slate-700 dark:text-white">Framework <span class="text-primary">Guide</span></h1>
5
5
  <p class="text-xl text-slate-500 max-w-2xl leading-relaxed">Everything you need to know about building premium web applications with the One-For-All framework.</p>
6
6
  </div>
@@ -19,6 +19,7 @@
19
19
  <a href="#api" class="block px-4 py-2 rounded-xl hover:bg-primary/5 text-slate-600 hover:text-primary transition-all font-semibold border-l-2 border-transparent hover:border-primary">API & Modern Web</a>
20
20
  <a href="#advanced" class="block px-4 py-2 rounded-xl hover:bg-primary/5 text-slate-600 hover:text-primary transition-all font-semibold border-l-2 border-transparent hover:border-primary">Mailers & Background Jobs</a>
21
21
  <a href="#testing" class="block px-4 py-2 rounded-xl hover:bg-primary/5 text-slate-600 hover:text-primary transition-all font-semibold border-l-2 border-transparent hover:border-primary">Automated Testing</a>
22
+ <a href="#plugins" class="block px-4 py-2 rounded-xl hover:bg-primary/5 text-slate-600 hover:text-primary transition-all font-semibold border-l-2 border-transparent hover:border-primary">Plugin System</a>
22
23
  <a href="#deploy" class="block px-4 py-2 rounded-xl hover:bg-primary/5 text-slate-600 hover:text-primary transition-all font-semibold border-l-2 border-transparent hover:border-primary">Deployment</a>
23
24
  </nav>
24
25
  </aside>
@@ -115,6 +116,10 @@
115
116
  <td class="px-6 py-4 font-mono text-primary">./ofa g test NAME</td>
116
117
  <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs">Generates a Minitest unit test file.</td>
117
118
  </tr>
119
+ <tr>
120
+ <td class="px-6 py-4 font-mono text-primary">./ofa g plugin NAME</td>
121
+ <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs">Scaffolds a new plugin structure in <code>/plugins</code>.</td>
122
+ </tr>
118
123
 
119
124
  <tr class="bg-slate-500/5"><td colspan="2" class="px-6 py-2 text-[10px] font-black uppercase tracking-tighter text-slate-400">Customization</td></tr>
120
125
  <tr>
@@ -303,6 +308,26 @@ resources :posts
303
308
  <li>• <strong>Image Handling:</strong> Drag-and-drop images directly into the editor with auto-storage sync.</li>
304
309
  <li>• <strong>Code Blocks:</strong> Native support for code blocks with one-click "Copy" functionality.</li>
305
310
  </ul>
311
+
312
+ <h4 class="font-bold text-lg mt-8 mb-4">🔍 Activity Logs (Audit Trail)</h4>
313
+ <p class="text-sm text-slate-500 mb-4">Track every action taken by users in the CMS. Automatically logs creation, updates, and deletions of pages and posts.</p>
314
+ <ul class="text-xs space-y-2 text-slate-600 dark:text-slate-400">
315
+ <li>• <strong>User Identification:</strong> Logs include the username and avatar of the person who performed the action.</li>
316
+ <li>• <strong>Detailed Context:</strong> Records specific changes, such as new slugs or modified categories.</li>
317
+ <li>• <strong>Database Agnostic:</strong> Works seamlessly with both SQL and MongoDB storage.</li>
318
+ </ul>
319
+
320
+ <h4 class="font-bold text-lg mt-8 mb-4">🚧 Maintenance Mode</h4>
321
+ <p class="text-sm text-slate-500 mb-4">Instantly activate a professional "Under Construction" page for your visitors using the CLI. Perfect for updates or migrations.</p>
322
+ <div class="p-4 bg-white/5 border border-white/10 rounded-2xl mb-4 font-mono text-sm">
323
+ ./ofa maintenance on<br>
324
+ ./ofa maintenance off
325
+ </div>
326
+ <ul class="text-xs space-y-2 text-slate-600 dark:text-slate-400">
327
+ <li>• <strong>Admin Bypass:</strong> Logged-in administrators can still view and manage the site while maintenance is active.</li>
328
+ <li>• <strong>Premium Design:</strong> Features a high-quality Glassmorphism roket animation to keep your brand looking professional even when offline.</li>
329
+ <li>• <strong>Dynamic Updates:</strong> Changes take effect instantly without requiring a server restart.</li>
330
+ </ul>
306
331
  </div>
307
332
  </section>
308
333
 
@@ -405,6 +430,33 @@ resources :posts
405
430
  </div>
406
431
  </section>
407
432
 
433
+ <!-- Plugin System -->
434
+ <section id="plugins" class="glass-panel p-8 md:p-12 text-left bg-gradient-to-br from-primary/10 to-transparent">
435
+ <h2 class="text-3xl font-black tracking-tight mb-6 flex items-center gap-3">
436
+ <i class="fas fa-plug text-primary"></i> Plugin System
437
+ </h2>
438
+ <div class="markdown-content">
439
+ <p>OFA v4.5.0 introduces a powerful <strong>Plugin System</strong> that allows you to extend the framework without modifying the core files. Plugins are located in the <code>/plugins</code> directory.</p>
440
+
441
+ <h4 class="font-bold text-lg mt-8 mb-4">🛠️ Working with Plugins</h4>
442
+ <div class="p-4 bg-white/5 border border-white/10 rounded-2xl mb-4 font-mono text-sm">
443
+ ./ofa g plugin my_awesome_feature
444
+ </div>
445
+ <p class="text-sm text-slate-500 mb-4">Each plugin has an <code>init.rb</code> file where you can register hooks and routes:</p>
446
+ <div class="p-4 bg-slate-900 rounded-2xl font-mono text-xs text-green-400 mb-6">
447
+ <pre>
448
+ OFA.on_boot do
449
+ puts "Plugin Loaded!"
450
+ end
451
+
452
+ OFA.add_route(:get, "/my-plugin") do |req, res|
453
+ res.body << "Hello from Plugin!"
454
+ end
455
+ </pre>
456
+ </div>
457
+ </div>
458
+ </section>
459
+
408
460
  <!-- Deployment -->
409
461
  <section id="deploy" class="glass-panel p-8 md:p-12 text-left border-primary/20">
410
462
  <h2 class="text-3xl font-black tracking-tight mb-6 flex items-center gap-3">
data/app/views/index.erb CHANGED
@@ -1,6 +1,6 @@
1
1
  <div class="text-center space-y-6 max-w-2xl mx-auto py-12">
2
2
  <div class="inline-flex items-center px-4 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider animate-pulse">
3
- Framework Version 4.4.0
3
+ Framework Version 4.5.0
4
4
  </div>
5
5
 
6
6
  <h1 class="text-5xl md:text-7xl font-black tracking-tight leading-tight">
@@ -0,0 +1,143 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Under Construction | One-For-All</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --primary: #6366f1;
11
+ --primary-glow: rgba(99, 102, 241, 0.5);
12
+ --bg: #0f172a;
13
+ --glass: rgba(255, 255, 255, 0.05);
14
+ --glass-border: rgba(255, 255, 255, 0.1);
15
+ }
16
+
17
+ * {
18
+ margin: 0;
19
+ padding: 0;
20
+ box-sizing: border-box;
21
+ font-family: 'Outfit', sans-serif;
22
+ }
23
+
24
+ body {
25
+ background: var(--bg);
26
+ color: white;
27
+ height: 100vh;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ overflow: hidden;
32
+ background-image:
33
+ radial-gradient(circle at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 40%),
34
+ radial-gradient(circle at 80% 80%, rgba(236, 72, 153, 0.15) 0%, transparent 40%);
35
+ }
36
+
37
+ .container {
38
+ text-align: center;
39
+ padding: 3rem;
40
+ background: var(--glass);
41
+ backdrop-filter: blur(20px);
42
+ border: 1px solid var(--glass-border);
43
+ border-radius: 2rem;
44
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
45
+ max-width: 600px;
46
+ width: 90%;
47
+ animation: fadeIn 1s ease-out;
48
+ }
49
+
50
+ @keyframes fadeIn {
51
+ from { opacity: 0; transform: translateY(20px); }
52
+ to { opacity: 1; transform: translateY(0); }
53
+ }
54
+
55
+ .icon {
56
+ font-size: 5rem;
57
+ margin-bottom: 1.5rem;
58
+ display: inline-block;
59
+ animation: float 3s ease-in-out infinite;
60
+ }
61
+
62
+ @keyframes float {
63
+ 0%, 100% { transform: translateY(0); }
64
+ 50% { transform: translateY(-10px); }
65
+ }
66
+
67
+ h1 {
68
+ font-size: 2.5rem;
69
+ font-weight: 600;
70
+ margin-bottom: 1rem;
71
+ background: linear-gradient(to right, #818cf8, #f472b6);
72
+ -webkit-background-clip: text;
73
+ -webkit-text-fill-color: transparent;
74
+ }
75
+
76
+ p {
77
+ font-size: 1.1rem;
78
+ color: #94a3b8;
79
+ line-height: 1.6;
80
+ margin-bottom: 2rem;
81
+ }
82
+
83
+ .loader {
84
+ width: 100%;
85
+ height: 4px;
86
+ background: rgba(255, 255, 255, 0.1);
87
+ border-radius: 2px;
88
+ overflow: hidden;
89
+ margin-bottom: 2rem;
90
+ }
91
+
92
+ .loader-bar {
93
+ width: 40%;
94
+ height: 100%;
95
+ background: var(--primary);
96
+ box-shadow: 0 0 15px var(--primary-glow);
97
+ border-radius: 2px;
98
+ animation: loading 2s infinite ease-in-out;
99
+ }
100
+
101
+ @keyframes loading {
102
+ 0% { transform: translateX(-100%); }
103
+ 100% { transform: translateX(300%); }
104
+ }
105
+
106
+ .footer {
107
+ font-size: 0.9rem;
108
+ color: #64748b;
109
+ }
110
+
111
+ .admin-link {
112
+ margin-top: 2rem;
113
+ display: block;
114
+ color: var(--primary);
115
+ text-decoration: none;
116
+ font-weight: 500;
117
+ opacity: 0.5;
118
+ transition: opacity 0.3s;
119
+ }
120
+
121
+ .admin-link:hover {
122
+ opacity: 1;
123
+ }
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <div class="container">
128
+ <div class="icon">🚀</div>
129
+ <h1>Under Construction</h1>
130
+ <p>We're working hard to bring you something amazing. Our site is currently undergoing scheduled maintenance. Please check back soon!</p>
131
+
132
+ <div class="loader">
133
+ <div class="loader-bar"></div>
134
+ </div>
135
+
136
+ <div class="footer">
137
+ &copy; <%= Time.now.year %> One-For-All Framework. All rights reserved.
138
+ </div>
139
+
140
+ <a href="/login" class="admin-link">Admin Login</a>
141
+ </div>
142
+ </body>
143
+ </html>
data/bin/ofa CHANGED
@@ -33,7 +33,7 @@ def help
33
33
  puts " / __ \\/ ____/ / | "
34
34
  puts " / / / / /_ / /| | Framework "
35
35
  puts "/ /_/ / __/ / ___ | Premium MVC "
36
- puts "\\____/_/ /_/ |_| v4.4.0 "
36
+ puts "\\____/_/ /_/ |_| v4.5.0 "
37
37
  puts " "
38
38
  puts "✨ One-For-All Framework CLI ✨"
39
39
  puts "-----------------------------"
@@ -61,6 +61,7 @@ def help
61
61
  puts " ofa doctor - Check system health (DB, config, gems)"
62
62
  puts " ofa routes - List all registered routes"
63
63
  puts " ofa swagger - Auto-generate OpenAPI/Swagger documentation"
64
+ puts " ofa maintenance ACTION - Toggle maintenance mode (on/off)"
64
65
  puts " ofa task NAME - Run a background task"
65
66
  puts " ofa test - Run all application tests"
66
67
  puts " ofa run - Start the application server"
@@ -247,7 +248,7 @@ when 'init'
247
248
  puts " / __ \\/ ____/ / | "
248
249
  puts " / / / / /_ / /| | Framework "
249
250
  puts "/ /_/ / __/ / ___ | Premium MVC "
250
- puts "\\____/_/ /_/ |_| v4.4.0 "
251
+ puts "\\____/_/ /_/ |_| v4.5.0 "
251
252
  puts " "
252
253
  puts "Initializing One-For-All project as '#{app_type}' in #{PROJECT_ROOT}..."
253
254
 
@@ -327,12 +328,16 @@ when 'init'
327
328
  # Middleware setup
328
329
  use EksCent::Middleware::ShowExceptions
329
330
  use EksCent::Middleware::Session, secret: EksCent.secret_key_base
331
+ use MaintenanceMiddleware
330
332
  use AuthMiddleware
331
333
  use JwtMiddleware
332
334
  use CSRFMiddleware
333
335
  use EksCent::Middleware::Static, root: 'public'
334
336
  use EksCent::Middleware::Logger
335
337
 
338
+ # Apply plugin routes
339
+ OFA.apply_pending_routes(ROUTES) if defined?(OFA)
340
+
336
341
  run ROUTES
337
342
  RUBY
338
343
  File.write(File.join(PROJECT_ROOT, 'config.ru'), config_ru_content)
@@ -619,8 +624,28 @@ when 'g'
619
624
  content = "<h1>#{name}</h1>\n<p>Author: #{options[:author] || 'Admin'}</p>\n<p>Category: #{options[:category] || 'General'}</p>"
620
625
  File.write(file_name, content)
621
626
  puts "Created post: #{file_name}"
627
+ elsif sub == 'plugin' && name
628
+ dir_path = File.join(PROJECT_ROOT, "plugins/#{name.downcase}")
629
+ FileUtils.mkdir_p(dir_path)
630
+ file_name = File.join(dir_path, "init.rb")
631
+ content = <<~RUBY
632
+ # Plugin: #{name.capitalize}
633
+ # Created at: #{Time.now}
634
+
635
+ OFA.on_boot do
636
+ puts "🚀 Plugin #{name.capitalize} loaded!"
637
+ end
638
+
639
+ # Example route added by plugin
640
+ # OFA.add_route(:get, '/#{name.downcase}') do |req, res|
641
+ # res.body << "Hello from #{name.capitalize} Plugin!"
642
+ # end
643
+ RUBY
644
+ File.write(file_name, content)
645
+ puts "Created Plugin: #{dir_path}"
646
+ puts "💡 Edit #{file_name} to start building your plugin."
622
647
  else
623
- puts "Usage: ofa g [controller|api|model|migration|mailer|task|test|post] NAME"
648
+ puts "Usage: ofa g [controller|api|model|migration|mailer|task|test|post|plugin] NAME"
624
649
  end
625
650
 
626
651
  when 'feature'
@@ -681,6 +706,19 @@ when 'storage'
681
706
  File.write(config_path, JSON.pretty_generate(config))
682
707
  puts "Application storage set to '#{name}'."
683
708
 
709
+ when 'maintenance'
710
+ ensure_initialized!
711
+ action = ARGV.shift
712
+ unless %w[on off].include?(action)
713
+ puts "Usage: ofa maintenance [on|off]"
714
+ exit 1
715
+ end
716
+ config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
717
+ config = JSON.parse(File.read(config_path))
718
+ config['maintenance'] = (action == 'on')
719
+ File.write(config_path, JSON.pretty_generate(config))
720
+ puts "Maintenance mode has been turned #{action}."
721
+
684
722
  when 'reset-password'
685
723
  ENV['SKIP_MODELS'] = '1'
686
724
  Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
@@ -823,7 +861,7 @@ when 'console'
823
861
  puts " / __ \\/ ____/ / | "
824
862
  puts " / / / / /_ / /| | Framework "
825
863
  puts "/ /_/ / __/ / ___ | Console (REPL) "
826
- puts "\\____/_/ /_/ |_| v4.4.0 "
864
+ puts "\\____/_/ /_/ |_| v4.5.0 "
827
865
  puts " "
828
866
  puts "✨ Loading environment... (Type 'exit' to quit)"
829
867
 
data/config/boot.rb CHANGED
@@ -9,6 +9,8 @@ begin
9
9
  rescue LoadError
10
10
  end
11
11
 
12
+ require_relative '../lib/plugin_helper'
13
+
12
14
  # Basic project structure constants
13
15
  APP_ROOT ||= File.expand_path('..', __dir__)
14
16
 
@@ -110,6 +112,14 @@ framework_app = File.expand_path('../app', __dir__)
110
112
  end
111
113
  end
112
114
 
115
+ # Load Plugins
116
+ plugins_dir = File.join(APP_ROOT, 'plugins')
117
+ if File.directory?(plugins_dir)
118
+ Dir.glob(File.join(plugins_dir, '**', '*.rb')).each do |plugin_file|
119
+ require plugin_file
120
+ end
121
+ end
122
+
113
123
  # ─── Routes DSL Extensions ─────────────────────────────────────────────────────
114
124
  # Extends EksCent::Router with a `resources` helper that auto-generates RESTful
115
125
  # routes (index, show, new, create, edit, update, destroy) for a given resource.
@@ -151,3 +161,6 @@ end
151
161
 
152
162
  # Load routes
153
163
  require_relative 'routes'
164
+
165
+ # Run Plugin Boot Hooks
166
+ OFA.run_boot_hooks if defined?(OFA)
data/config/database.rb CHANGED
@@ -130,6 +130,18 @@ if DB
130
130
  end
131
131
  end
132
132
 
133
+ unless DB.table_exists?(:activity_logs)
134
+ DB.create_table :activity_logs do
135
+ primary_key :id
136
+ String :user_id
137
+ String :action, null: false
138
+ String :target_type
139
+ String :target_id
140
+ Text :details
141
+ DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
142
+ end
143
+ end
144
+
133
145
  # Quick Migration for existing tables
134
146
  if DB.table_exists?(:pages)
135
147
  DB.alter_table(:pages) { add_column :is_active, TrueClass, default: true unless DB[:pages].columns.include?(:is_active) }
data/config/features.json CHANGED
@@ -4,5 +4,6 @@
4
4
  "type": "landing_page",
5
5
  "theme": "light_sidebar",
6
6
  "storage": "cloudinary",
7
- "rich_text": true
7
+ "rich_text": true,
8
+ "maintenance": false
8
9
  }
data/config/routes.rb CHANGED
@@ -61,6 +61,10 @@ ROUTES = EksCent::Router.new do
61
61
  DashboardController.new(req, res).upload
62
62
  end
63
63
 
64
+ get '/dashboard/logs' do |req, res|
65
+ ActivityLogsController.new(req, res).index
66
+ end
67
+
64
68
  # Resourceful CMS Routes
65
69
  resources :pages, prefix: '/dashboard'
66
70
  resources :posts, prefix: '/dashboard'
data/config.ru CHANGED
@@ -3,9 +3,13 @@ require_relative 'config/boot'
3
3
  # Middleware setup
4
4
  use EksCent::Middleware::ShowExceptions
5
5
  use EksCent::Middleware::Session, secret: EksCent.secret_key_base
6
+ use MaintenanceMiddleware
6
7
  use AuthMiddleware
7
8
  use CSRFMiddleware
8
9
  use EksCent::Middleware::Static, root: 'public'
9
10
  use EksCent::Middleware::Logger
10
11
 
12
+ # Apply plugin routes
13
+ OFA.apply_pending_routes(ROUTES) if defined?(OFA)
14
+
11
15
  run ROUTES
data/db/data.sqlite3 CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  Sequel.migration do
2
2
  change do
3
- create_table(:products) do
3
+ create_table?(:products) do
4
4
  primary_key :id
5
5
  String :name, null: false
6
6
  String :slug, null: false, unique: true
@@ -0,0 +1,13 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:activity_logs) do
4
+ primary_key :id
5
+ String :user_id
6
+ String :action, null: false
7
+ String :target_type
8
+ String :target_id
9
+ Text :details
10
+ DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ Sequel.migration do
2
+ change do
3
+ alter_table(:activity_logs) do
4
+ set_column_type :user_id, String
5
+ end
6
+ end
7
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: one-for-all-framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.4.0
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ishikawa Uta
@@ -200,6 +200,7 @@ files:
200
200
  - LICENSE
201
201
  - Procfile
202
202
  - README.md
203
+ - app/controllers/activity_logs_controller.rb
203
204
  - app/controllers/api_controller.rb
204
205
  - app/controllers/application_controller.rb
205
206
  - app/controllers/auth_controller.rb
@@ -214,6 +215,8 @@ files:
214
215
  - app/middleware/auth_middleware.rb
215
216
  - app/middleware/csrf_middleware.rb
216
217
  - app/middleware/jwt_middleware.rb
218
+ - app/middleware/maintenance_middleware.rb
219
+ - app/models/activity_log.rb
217
220
  - app/models/page.rb
218
221
  - app/models/post.rb
219
222
  - app/models/product.rb
@@ -221,6 +224,7 @@ files:
221
224
  - app/models/user.rb
222
225
  - app/views/blog_home.erb
223
226
  - app/views/cart_index.erb
227
+ - app/views/cms/activity_logs.erb
224
228
  - app/views/cms/pages_form.erb
225
229
  - app/views/cms/pages_index.erb
226
230
  - app/views/cms/posts_form.erb
@@ -235,6 +239,7 @@ files:
235
239
  - app/views/index.erb
236
240
  - app/views/layout.erb
237
241
  - app/views/login.erb
242
+ - app/views/maintenance.erb
238
243
  - app/views/page.erb
239
244
  - app/views/portfolio.erb
240
245
  - app/views/post.erb
@@ -255,6 +260,8 @@ files:
255
260
  - db/data.sqlite3
256
261
  - db/development.sqlite3
257
262
  - db/migrations/20260502000000_create_products.rb
263
+ - db/migrations/20260507000000_create_activity_logs.rb
264
+ - db/migrations/20260507000001_fix_activity_logs_user_id.rb
258
265
  - ofa
259
266
  - public/css/cms.css
260
267
  - public/images/logo.jpg