one-for-all-framework 1.0.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/.env.example +4 -0
- data/Dockerfile +19 -0
- data/Gemfile +20 -0
- data/LICENSE +21 -0
- data/Procfile +1 -0
- data/README.md +99 -0
- data/app/controllers/api_controller.rb +19 -0
- data/app/controllers/application_controller.rb +77 -0
- data/app/controllers/auth_controller.rb +35 -0
- data/app/controllers/dashboard_controller.rb +41 -0
- data/app/controllers/pages_controller.rb +49 -0
- data/app/controllers/posts_controller.rb +48 -0
- data/app/controllers/projects_controller.rb +48 -0
- data/app/helpers/cloudinary_helper.rb +14 -0
- data/app/middleware/auth_middleware.rb +52 -0
- data/app/middleware/csrf_middleware.rb +27 -0
- data/app/models/page.rb +8 -0
- data/app/models/post.rb +8 -0
- data/app/models/project.rb +7 -0
- data/app/models/user.rb +83 -0
- data/app/views/blog_home.erb +70 -0
- data/app/views/cms/pages_form.erb +156 -0
- data/app/views/cms/pages_index.erb +104 -0
- data/app/views/cms/posts_form.erb +182 -0
- data/app/views/cms/posts_index.erb +100 -0
- data/app/views/cms/projects_form.erb +176 -0
- data/app/views/cms/projects_index.erb +96 -0
- data/app/views/dashboard.erb +122 -0
- data/app/views/docs.erb +140 -0
- data/app/views/error.erb +159 -0
- data/app/views/index.erb +70 -0
- data/app/views/layout.erb +251 -0
- data/app/views/login.erb +45 -0
- data/app/views/page.erb +21 -0
- data/app/views/portfolio.erb +73 -0
- data/app/views/post.erb +33 -0
- data/app/views/posts/hello_world.erb +3 -0
- data/app/views/project.erb +39 -0
- data/bin/ofa +499 -0
- data/config/boot.rb +134 -0
- data/config/database.json +4 -0
- data/config/database.rb +142 -0
- data/config/features.json +7 -0
- data/config/locales/en.json +6 -0
- data/config/locales/id.json +6 -0
- data/config/routes.rb +87 -0
- data/config.eks +11 -0
- data/config.ru +11 -0
- data/db/data.sqlite3 +0 -0
- data/db/development.sqlite3 +0 -0
- data/ofa +2 -0
- data/public/css/cms.css +134 -0
- data/public/images/logo.jpg +0 -0
- data/public/images/logo.png +0 -0
- metadata +259 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7da16da0dd7fd64c3418a570d9d9c2f366fe487a464cd524214ead23b249a932
|
|
4
|
+
data.tar.gz: 43e95c18a2a0adc29c1c1fb39f95e175a11db8dd9fb70979b9ee04156d49c6f7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7e770e771c8cfd8e8e2c7d62a6b46e3a06cada5144a60a35c3d5348a06040a59158de1c2b9c807026c982fd74ea4c1e0f1be8e114f36f91b420955706a9b2e50
|
|
7
|
+
data.tar.gz: 28c6bc05a32ddf5af5c78e74939adc3886b811c31a812e7fcba9d1b6b437f8f34bc7858917e1f7ef8463dbe93c93c05b7b8927e074b03838cbe1966fc7760761
|
data/.env.example
ADDED
data/Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
FROM ruby:3.2-slim
|
|
2
|
+
|
|
3
|
+
# Install dependencies
|
|
4
|
+
RUN apt-get update -qq && apt-get install -y build-essential libsqlite3-dev
|
|
5
|
+
|
|
6
|
+
# Set working directory
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
# Copy files
|
|
10
|
+
COPY . .
|
|
11
|
+
|
|
12
|
+
# Install gems
|
|
13
|
+
RUN bundle install
|
|
14
|
+
|
|
15
|
+
# Expose port
|
|
16
|
+
EXPOSE 3000
|
|
17
|
+
|
|
18
|
+
# Start command
|
|
19
|
+
CMD ["./ofa", "run"]
|
data/Gemfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
|
+
|
|
3
|
+
gem 'eks-cent', '4.0.0'
|
|
4
|
+
gem 'eksa-server'
|
|
5
|
+
gem 'sequel'
|
|
6
|
+
gem 'sqlite3'
|
|
7
|
+
gem 'bcrypt'
|
|
8
|
+
gem 'json'
|
|
9
|
+
gem 'cloudinary'
|
|
10
|
+
gem 'mysql2'
|
|
11
|
+
gem 'pg'
|
|
12
|
+
gem 'mongo'
|
|
13
|
+
gem 'dotenv'
|
|
14
|
+
gem 'kramdown'
|
|
15
|
+
gem 'kramdown-parser-gfm'
|
|
16
|
+
|
|
17
|
+
group :test do
|
|
18
|
+
gem 'eksa-mination'
|
|
19
|
+
gem 'rack-test'
|
|
20
|
+
end
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ishikawa Uta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
web: ./ofa run
|
data/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="public/images/logo.png" width="500" height="500" alt="OFA Framework Logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# ⚡ One-For-All (OFA) Framework
|
|
6
|
+
|
|
7
|
+
[](https://www.ruby-lang.org/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[]()
|
|
10
|
+
|
|
11
|
+
**One-For-All (OFA)** is a premium, ultra-fast Ruby MVC framework designed for developers who value both high performance and modern aesthetics. Built on the powerful **Eks-Cent** engine and optimized with **Eksa Server**, OFA provides a production-ready foundation with a stunning "Glassmorphism" UI out of the box.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Why One-For-All?
|
|
16
|
+
|
|
17
|
+
- **💎 Premium Aesthetics**: Beautiful Glassmorphism design system included by default with smooth dark/light mode transitions.
|
|
18
|
+
- **🚀 Blazing Fast**: Built on a modular Nio4r-powered engine for minimal overhead and instant boot times.
|
|
19
|
+
- **📂 Multi-Database**: Seamlessly switch between SQLite, MySQL, MariaDB, and MongoDB Atlas.
|
|
20
|
+
- **🛠️ Developer First**: A robust CLI (`ofa`) that handles everything from scaffolding to deployment.
|
|
21
|
+
- **🔐 Enterprise Ready**: Built-in CSRF protection, secure session management, and input validation.
|
|
22
|
+
- **🌐 Global Support**: Multi-language (I18n) support and SEO optimization ready.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🚀 Getting Started
|
|
27
|
+
|
|
28
|
+
### 1. Prerequisites
|
|
29
|
+
Ensure you have Ruby 3.0+ and Bundler installed on your system.
|
|
30
|
+
|
|
31
|
+
### 2. Installation
|
|
32
|
+
Clone the repository and install dependencies:
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/ishikawauta/one-for-all-framework.git
|
|
35
|
+
cd one-for-all-framework
|
|
36
|
+
bundle install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 3. Quick Initialization
|
|
40
|
+
Initialize your project environment and database:
|
|
41
|
+
```bash
|
|
42
|
+
./ofa init
|
|
43
|
+
```
|
|
44
|
+
*The interactive wizard will help you configure your database and cloud storage (Cloudinary).*
|
|
45
|
+
|
|
46
|
+
### 4. Run the Engine
|
|
47
|
+
Launch your development server:
|
|
48
|
+
```bash
|
|
49
|
+
./ofa run
|
|
50
|
+
```
|
|
51
|
+
Your app is now live at `http://localhost:3000` ⚡
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🛠️ CLI Power Tools
|
|
56
|
+
|
|
57
|
+
The `ofa` CLI is your best friend. Use it to manage your entire application lifecycle:
|
|
58
|
+
|
|
59
|
+
| Command | Description |
|
|
60
|
+
| :--- | :--- |
|
|
61
|
+
| `ofa new NAME` | Create a brand new project from scratch. |
|
|
62
|
+
| `ofa g controller Name` | Generate a RESTful controller. |
|
|
63
|
+
| `ofa g model Name` | Generate a database model and migration. |
|
|
64
|
+
| `ofa db migrate` | Sync your database with the latest schema. |
|
|
65
|
+
| `ofa type [blog\|portfolio]` | Switch application mode instantly. |
|
|
66
|
+
| `ofa theme [dark\|light]` | Toggle between premium UI themes. |
|
|
67
|
+
| `ofa storage cloudinary` | Enable automated Cloudinary image hosting. |
|
|
68
|
+
| `ofa reset-password USR PWD`| Securely reset admin credentials. |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🏗️ Architecture
|
|
73
|
+
|
|
74
|
+
OFA follows a strict **MVC (Model-View-Controller)** pattern:
|
|
75
|
+
|
|
76
|
+
- **Models**: Powered by **Sequel** for SQL and **Mongo Ruby Driver** for NoSQL.
|
|
77
|
+
- **Views**: High-performance **ERB** templates with a modular design system.
|
|
78
|
+
- **Controllers**: Lightweight logic handlers with built-in validation helpers.
|
|
79
|
+
- **Middleware**: Custom authentication and session sliding expiration (8-hour default).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 🚢 Deployment
|
|
84
|
+
|
|
85
|
+
One-For-All is optimized for modern cloud platforms:
|
|
86
|
+
|
|
87
|
+
- **Railway / Heroku**: Uses the included `Procfile` for automatic detection.
|
|
88
|
+
- **Docker**: A lightweight `Dockerfile` based on `ruby:3.2-slim` is provided.
|
|
89
|
+
- **VPS**: Can be run behind Nginx/Apache using the `ofa run` command.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🤝 Contributing
|
|
94
|
+
|
|
95
|
+
We welcome contributions! Please feel free to submit Pull Requests or report issues on the [GitHub repository](https://github.com/ishikawauta/one-for-all-framework).
|
|
96
|
+
|
|
97
|
+
## 📄 License
|
|
98
|
+
|
|
99
|
+
This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative 'application_controller'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
class ApiController < ApplicationController
|
|
5
|
+
def render_json(data, status: 200)
|
|
6
|
+
@res.status = status
|
|
7
|
+
@res.headers['Content-Type'] = 'application/json'
|
|
8
|
+
@res.body << data.to_json
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def status
|
|
12
|
+
render_json({ status: 'ok', version: '1.0.0', type: FEATURES_CONFIG['type'] })
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def halt_error(message, status: 400)
|
|
16
|
+
render_json({ error: message }, status: status)
|
|
17
|
+
throw(:halt)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
class ApplicationController
|
|
2
|
+
attr_reader :req, :res
|
|
3
|
+
|
|
4
|
+
def initialize(req, res)
|
|
5
|
+
@req = req
|
|
6
|
+
@res = res
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def params
|
|
10
|
+
@req.params
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def session
|
|
14
|
+
@req.env['eks_cent.session'] ||= @req.env['rack.session'] || {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def render(template, **locals)
|
|
18
|
+
# Pass controller instance variables to the view
|
|
19
|
+
instance_variables.each do |var|
|
|
20
|
+
locals[var.to_s.delete('@').to_sym] ||= instance_variable_get(var)
|
|
21
|
+
end
|
|
22
|
+
@res.render(template, **locals)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def redirect_to(path)
|
|
26
|
+
@res.status = 302
|
|
27
|
+
@res.headers['Location'] = path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Validation Helper
|
|
31
|
+
def validate!(required_params)
|
|
32
|
+
missing = required_params.select { |p| params[p.to_s].nil? || params[p.to_s].empty? }
|
|
33
|
+
unless missing.empty?
|
|
34
|
+
@res.status = 400
|
|
35
|
+
render 'error', layout: false, message: "Missing required parameters: #{missing.join(', ')}"
|
|
36
|
+
throw(:halt)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# CSRF Helper for views
|
|
41
|
+
def csrf_token
|
|
42
|
+
@req.env['eks_cent.csrf_token']
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def boolean_param(val)
|
|
46
|
+
['1', 'true', 'on'].include?(val.to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def delete_from_storage(url)
|
|
50
|
+
return unless url && !url.empty?
|
|
51
|
+
if url.include?('cloudinary.com')
|
|
52
|
+
# Extract public_id from Cloudinary URL
|
|
53
|
+
# Example: https://res.cloudinary.com/demo/image/upload/v12345/sample.jpg -> sample
|
|
54
|
+
public_id = url.split('/').last.split('.').first
|
|
55
|
+
begin
|
|
56
|
+
require 'cloudinary'
|
|
57
|
+
Cloudinary::Uploader.destroy(public_id)
|
|
58
|
+
rescue => e
|
|
59
|
+
puts "⚠ Error deleting from Cloudinary: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
elsif url.start_with?('/img/uploads/')
|
|
62
|
+
path = File.join(APP_ROOT, 'public', url)
|
|
63
|
+
FileUtils.rm(path) if File.exist?(path) rescue nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def delete_all_images_from_content(content)
|
|
68
|
+
return unless content
|
|
69
|
+
# Find all Cloudinary URLs in markdown
|
|
70
|
+
urls = content.scan(/https?:\/\/res\.cloudinary\.com\/[^\/]+\/image\/upload\/[^\s\)]+/)
|
|
71
|
+
urls.each { |url| delete_from_storage(url) }
|
|
72
|
+
|
|
73
|
+
# Also find local uploads
|
|
74
|
+
local_urls = content.scan(/\/img\/uploads\/[^\s\)]+/)
|
|
75
|
+
local_urls.each { |url| delete_from_storage(url) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require_relative 'application_controller'
|
|
2
|
+
|
|
3
|
+
class AuthController < ApplicationController
|
|
4
|
+
def show_login
|
|
5
|
+
if session['user_id']
|
|
6
|
+
redirect_to '/dashboard'
|
|
7
|
+
return
|
|
8
|
+
end
|
|
9
|
+
reason = req.params['reason']
|
|
10
|
+
error = reason == 'expired' ? 'Your session has expired. Please log in again.' : nil
|
|
11
|
+
render 'login', title: 'Login - One-For-All', error: error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def login
|
|
15
|
+
username = req.params['username']
|
|
16
|
+
password = req.params['password']
|
|
17
|
+
|
|
18
|
+
user = User.authenticate(username, password)
|
|
19
|
+
if user
|
|
20
|
+
# Set session keys
|
|
21
|
+
session['user_id'] = user.id
|
|
22
|
+
session['username'] = user.username
|
|
23
|
+
session['last_active_at'] = Time.now.to_i
|
|
24
|
+
|
|
25
|
+
redirect_to '/dashboard'
|
|
26
|
+
else
|
|
27
|
+
render 'login', title: 'Login - One-For-All', error: 'Invalid credentials'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def logout
|
|
32
|
+
['user_id', :user_id, 'username', :username, 'last_active_at', :last_active_at].each { |k| session.delete(k) }
|
|
33
|
+
redirect_to '/login'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require_relative 'application_controller'
|
|
2
|
+
require 'cloudinary'
|
|
3
|
+
|
|
4
|
+
class DashboardController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@type = FEATURES_CONFIG['type']
|
|
7
|
+
@stats = {
|
|
8
|
+
pages: Page.count,
|
|
9
|
+
posts: Post.count,
|
|
10
|
+
projects: Project.count
|
|
11
|
+
}
|
|
12
|
+
render 'dashboard'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Image Upload Action
|
|
16
|
+
def upload
|
|
17
|
+
file = params['file']
|
|
18
|
+
if file && file[:tempfile]
|
|
19
|
+
if FEATURES_CONFIG['storage'] == 'cloudinary'
|
|
20
|
+
begin
|
|
21
|
+
upload = Cloudinary::Uploader.upload(file[:tempfile].path)
|
|
22
|
+
url = upload['secure_url']
|
|
23
|
+
@res.body << { url: url }.to_json
|
|
24
|
+
rescue => e
|
|
25
|
+
@res.status = 500
|
|
26
|
+
@res.body << { error: e.message }.to_json
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
filename = "#{Time.now.to_i}_#{file[:filename]}"
|
|
30
|
+
target = File.join(APP_ROOT, 'public/img/uploads', filename)
|
|
31
|
+
FileUtils.cp(file[:tempfile].path, target)
|
|
32
|
+
@res.body << { url: "/img/uploads/#{filename}" }.to_json
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
@res.status = 400
|
|
36
|
+
@res.body << { error: "No file uploaded" }.to_json
|
|
37
|
+
end
|
|
38
|
+
@res.headers['Content-Type'] = 'application/json'
|
|
39
|
+
throw(:halt)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require_relative 'application_controller'
|
|
2
|
+
|
|
3
|
+
class PagesController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@pages = Page.all
|
|
6
|
+
render 'cms/pages_index'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def new
|
|
10
|
+
@page = Page.new
|
|
11
|
+
render 'cms/pages_form'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
data = params['page']
|
|
16
|
+
data['is_active'] = boolean_param(data['is_active'])
|
|
17
|
+
data['is_nav'] = boolean_param(data['is_nav'])
|
|
18
|
+
@page = Page.new(data)
|
|
19
|
+
if @page.save
|
|
20
|
+
redirect_to '/dashboard/pages'
|
|
21
|
+
else
|
|
22
|
+
render 'cms/pages_form'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def edit
|
|
27
|
+
@page = Page[params['id']]
|
|
28
|
+
render 'cms/pages_form'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def update
|
|
32
|
+
@page = Page[params['id']]
|
|
33
|
+
data = params['page']
|
|
34
|
+
data['is_active'] = boolean_param(data['is_active'])
|
|
35
|
+
data['is_nav'] = boolean_param(data['is_nav'])
|
|
36
|
+
if @page.update(data)
|
|
37
|
+
redirect_to '/dashboard/pages'
|
|
38
|
+
else
|
|
39
|
+
render 'cms/pages_form'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def destroy
|
|
44
|
+
@page = Page[params['id']]
|
|
45
|
+
delete_all_images_from_content(@page.content) if @page.respond_to?(:content)
|
|
46
|
+
@page.destroy
|
|
47
|
+
redirect_to '/dashboard/pages'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative 'application_controller'
|
|
2
|
+
|
|
3
|
+
class PostsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@posts = Post.order(Sequel.desc(:created_at)).all
|
|
6
|
+
render 'cms/posts_index'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def new
|
|
10
|
+
@post = Post.new
|
|
11
|
+
render 'cms/posts_form'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
data = params['post']
|
|
16
|
+
data['is_active'] = boolean_param(data['is_active'])
|
|
17
|
+
@post = Post.new(data)
|
|
18
|
+
if @post.save
|
|
19
|
+
redirect_to '/dashboard/posts'
|
|
20
|
+
else
|
|
21
|
+
render 'cms/posts_form'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def edit
|
|
26
|
+
@post = Post[params['id']]
|
|
27
|
+
render 'cms/posts_form'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
@post = Post[params['id']]
|
|
32
|
+
data = params['post']
|
|
33
|
+
data['is_active'] = boolean_param(data['is_active'])
|
|
34
|
+
if @post.update(data)
|
|
35
|
+
redirect_to '/dashboard/posts'
|
|
36
|
+
else
|
|
37
|
+
render 'cms/posts_form'
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def destroy
|
|
42
|
+
@post = Post[params['id']]
|
|
43
|
+
delete_from_storage(@post.image_url) if @post.respond_to?(:image_url)
|
|
44
|
+
delete_all_images_from_content(@post.content) if @post.respond_to?(:content)
|
|
45
|
+
@post.destroy
|
|
46
|
+
redirect_to '/dashboard/posts'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative 'application_controller'
|
|
2
|
+
|
|
3
|
+
class ProjectsController < ApplicationController
|
|
4
|
+
def index
|
|
5
|
+
@projects = Project.order(Sequel.desc(:created_at)).all
|
|
6
|
+
render 'cms/projects_index'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def new
|
|
10
|
+
@project = Project.new
|
|
11
|
+
render 'cms/projects_form'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
data = params['project']
|
|
16
|
+
data['is_active'] = boolean_param(data['is_active'])
|
|
17
|
+
@project = Project.new(data)
|
|
18
|
+
if @project.save
|
|
19
|
+
redirect_to '/dashboard/projects'
|
|
20
|
+
else
|
|
21
|
+
render 'cms/projects_form'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def edit
|
|
26
|
+
@project = Project[params['id']]
|
|
27
|
+
render 'cms/projects_form'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
@project = Project[params['id']]
|
|
32
|
+
data = params['project']
|
|
33
|
+
data['is_active'] = boolean_param(data['is_active'])
|
|
34
|
+
if @project.update(data)
|
|
35
|
+
redirect_to '/dashboard/projects'
|
|
36
|
+
else
|
|
37
|
+
render 'cms/projects_form'
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def destroy
|
|
42
|
+
@project = Project[params['id']]
|
|
43
|
+
delete_from_storage(@project.image_url) if @project.respond_to?(:image_url)
|
|
44
|
+
delete_all_images_from_content(@project.description) if @project.respond_to?(:description)
|
|
45
|
+
@project.destroy
|
|
46
|
+
redirect_to '/dashboard/projects'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'cloudinary'
|
|
2
|
+
|
|
3
|
+
module CloudinaryHelper
|
|
4
|
+
def self.setup
|
|
5
|
+
Cloudinary.config do |config|
|
|
6
|
+
# In production, use ENV['CLOUDINARY_URL']
|
|
7
|
+
config.secure = true
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.upload(file_path)
|
|
12
|
+
Cloudinary::Uploader.upload(file_path)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class AuthMiddleware
|
|
2
|
+
# Session expires after 8 hours of inactivity
|
|
3
|
+
MAX_SESSION_AGE = 60 * 60 * 8
|
|
4
|
+
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
# Debug: Check what session keys are available
|
|
11
|
+
# puts "DEBUG KEYS: #{env.keys.select{|k| k.include?('session')}}"
|
|
12
|
+
|
|
13
|
+
env['eks_cent.session'] ||= env['rack.session'] || {}
|
|
14
|
+
session = env['eks_cent.session']
|
|
15
|
+
|
|
16
|
+
# Check for session expiry
|
|
17
|
+
user_id = session['user_id'] || session[:user_id]
|
|
18
|
+
last_active = session['last_active_at'] || session[:last_active_at]
|
|
19
|
+
|
|
20
|
+
if user_id && last_active
|
|
21
|
+
age = Time.now.to_i - last_active.to_i
|
|
22
|
+
if age > MAX_SESSION_AGE
|
|
23
|
+
# Clear session if expired
|
|
24
|
+
['user_id', :user_id, 'username', :username, 'last_active_at', :last_active_at].each { |k| session.delete(k) }
|
|
25
|
+
|
|
26
|
+
if requires_auth?(env['PATH_INFO'])
|
|
27
|
+
return [302, { 'Location' => '/login?reason=expired' }, []]
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
# Update last active time to extend session (sliding expiration)
|
|
31
|
+
session['last_active_at'] = Time.now.to_i
|
|
32
|
+
end
|
|
33
|
+
elsif user_id
|
|
34
|
+
# If logged in but no last_active_at (legacy session), set it now
|
|
35
|
+
session['last_active_at'] = Time.now.to_i
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# If the route requires authentication and user is not logged in
|
|
39
|
+
logged_in = session['user_id'] || session[:user_id] || session['username'] || session[:username]
|
|
40
|
+
if requires_auth?(env['PATH_INFO']) && !logged_in
|
|
41
|
+
return [302, { 'Location' => '/login' }, []]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@app.call(env)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def requires_auth?(path)
|
|
50
|
+
path.start_with?('/dashboard') || path.start_with?('/profile')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
class CSRFMiddleware
|
|
4
|
+
def initialize(app)
|
|
5
|
+
@app = app
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
req = Rack::Request.new(env)
|
|
10
|
+
env['eks_cent.session'] ||= env['rack.session'] || {}
|
|
11
|
+
session = env['eks_cent.session']
|
|
12
|
+
|
|
13
|
+
# Generate token if not exists
|
|
14
|
+
session['csrf_token'] ||= SecureRandom.hex(32)
|
|
15
|
+
env['eks_cent.csrf_token'] = session['csrf_token']
|
|
16
|
+
|
|
17
|
+
if ['POST', 'PUT', 'DELETE', 'PATCH'].include?(req.request_method)
|
|
18
|
+
token = req.params['csrf_token'] || req.env['HTTP_X_CSRF_TOKEN']
|
|
19
|
+
|
|
20
|
+
if token != session['csrf_token']
|
|
21
|
+
return [403, { 'Content-Type' => 'text/plain' }, ['Forbidden: CSRF Token Invalid']]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@app.call(env)
|
|
26
|
+
end
|
|
27
|
+
end
|
data/app/models/page.rb
ADDED
data/app/models/post.rb
ADDED