eksa-framework 3.3.3 → 3.4.3
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 +4 -4
- data/.eksa.json +4 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +10 -0
- data/README.md +18 -1
- data/_posts/2026-03-15-welcome-to-eksa-framework.md +145 -126
- data/app/views/about.html.erb +30 -10
- data/app/views/docs.html.erb +21 -2
- data/app/views/index.html.erb +1 -1
- data/exe/eksa +80 -0
- data/lib/eksa/auth_controller.rb +50 -0
- data/lib/eksa/cms_controller.rb +110 -0
- data/lib/eksa/controller.rb +32 -6
- data/lib/eksa/markdown_post.rb +11 -4
- data/lib/eksa/user.rb +47 -0
- data/lib/eksa/version.rb +1 -1
- data/lib/eksa/views/auth/login.html.erb +44 -0
- data/lib/eksa/views/auth/register.html.erb +53 -0
- data/lib/eksa/views/cms/edit.html.erb +63 -0
- data/lib/eksa/views/cms/index.html.erb +83 -0
- data/lib/eksa.rb +48 -3
- metadata +37 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module Eksa
|
|
2
|
+
class CmsController < Eksa::Controller
|
|
3
|
+
def index
|
|
4
|
+
return unless require_auth
|
|
5
|
+
|
|
6
|
+
@posts = Eksa::MarkdownPost.all(include_unpublished: true)
|
|
7
|
+
render_internal 'cms/index'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def edit
|
|
11
|
+
return unless require_auth
|
|
12
|
+
|
|
13
|
+
slug = params['slug']
|
|
14
|
+
@post = Eksa::MarkdownPost.find(slug)
|
|
15
|
+
|
|
16
|
+
if @post
|
|
17
|
+
render_internal 'cms/edit'
|
|
18
|
+
else
|
|
19
|
+
redirect_to "/cms", notice: "Error: Postingan tidak ditemukan."
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def update_post
|
|
24
|
+
return unless require_auth
|
|
25
|
+
|
|
26
|
+
slug = params['slug']
|
|
27
|
+
post_path = File.join(Eksa::MarkdownPost::POSTS_DIR, "#{slug}.md")
|
|
28
|
+
|
|
29
|
+
unless File.exist?(post_path)
|
|
30
|
+
return redirect_to "/cms", notice: "Error: Postingan tidak ditemukan."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Existing metadata fetching to preserve unmodified keys like 'date' if not handling them
|
|
34
|
+
# We parse form params
|
|
35
|
+
title = params['title']
|
|
36
|
+
category = params['category']
|
|
37
|
+
author = params['author']
|
|
38
|
+
image = params['image']
|
|
39
|
+
content = params['content']
|
|
40
|
+
|
|
41
|
+
# We'll re-read the original to preserve keys we aren't allowing to edit (like published status and date)
|
|
42
|
+
original_content = File.read(post_path, encoding: 'utf-8')
|
|
43
|
+
metadata = {}
|
|
44
|
+
|
|
45
|
+
if original_content =~ /\A(---\s*\r?\n.*?\r?\n)^(---\s*\r?\n?)/m
|
|
46
|
+
metadata = YAML.safe_load($1, permitted_classes: [Time]) || {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Update metadata with form values
|
|
50
|
+
metadata['title'] = title
|
|
51
|
+
metadata['category'] = category
|
|
52
|
+
metadata['author'] = author
|
|
53
|
+
metadata['image'] = image unless image.nil? || image.strip.empty?
|
|
54
|
+
|
|
55
|
+
# Dump new YAML
|
|
56
|
+
new_yaml = YAML.dump(metadata)
|
|
57
|
+
|
|
58
|
+
# Join with content
|
|
59
|
+
new_file_content = "#{new_yaml}---\n\n#{content}"
|
|
60
|
+
|
|
61
|
+
# Write changes
|
|
62
|
+
File.write(post_path, new_file_content, encoding: 'utf-8')
|
|
63
|
+
|
|
64
|
+
redirect_to "/cms", notice: "Postingan '#{title}' berhasil diperbarui."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def toggle_status
|
|
68
|
+
return unless require_auth
|
|
69
|
+
|
|
70
|
+
slug = params['slug']
|
|
71
|
+
post_path = File.join(Eksa::MarkdownPost::POSTS_DIR, "#{slug}.md")
|
|
72
|
+
|
|
73
|
+
if File.exist?(post_path)
|
|
74
|
+
content = File.read(post_path, encoding: 'utf-8')
|
|
75
|
+
|
|
76
|
+
# Simple string replacement for published status in front matter
|
|
77
|
+
if content.match?(/^published:\s*false/m)
|
|
78
|
+
new_content = content.sub(/^published:\s*false/m, "published: true")
|
|
79
|
+
status = "diaktifkan"
|
|
80
|
+
elsif content.match?(/^published:\s*true/m)
|
|
81
|
+
new_content = content.sub(/^published:\s*true/m, "published: false")
|
|
82
|
+
status = "dinonaktifkan"
|
|
83
|
+
else
|
|
84
|
+
# If no published tag exists, it's implicitly true, so we set it to false
|
|
85
|
+
new_content = content.sub(/\A(---\r?\n)/) { "#{$1}published: false\n" }
|
|
86
|
+
status = "dinonaktifkan"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
File.write(post_path, new_content)
|
|
90
|
+
redirect_to "/cms", notice: "Postingan #{File.basename(post_path)} berhasil #{status}."
|
|
91
|
+
else
|
|
92
|
+
redirect_to "/cms", notice: "Error: Postingan tidak ditemukan."
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def delete_post
|
|
97
|
+
return unless require_auth
|
|
98
|
+
|
|
99
|
+
slug = params['slug']
|
|
100
|
+
post_path = File.join(Eksa::MarkdownPost::POSTS_DIR, "#{slug}.md")
|
|
101
|
+
|
|
102
|
+
if File.exist?(post_path)
|
|
103
|
+
File.delete(post_path)
|
|
104
|
+
redirect_to "/cms", notice: "Postingan berhasil dihapus secara permanen."
|
|
105
|
+
else
|
|
106
|
+
redirect_to "/cms", notice: "Error: Postingan tidak ditemukan."
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/eksa/controller.rb
CHANGED
|
@@ -15,6 +15,23 @@ module Eksa
|
|
|
15
15
|
@request.params
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def session
|
|
19
|
+
@request.session
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def current_user
|
|
23
|
+
return @current_user if defined?(@current_user)
|
|
24
|
+
@current_user = session['user_id'] ? Eksa::User.find(session['user_id']) : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def require_auth
|
|
28
|
+
unless current_user
|
|
29
|
+
redirect_to "/auth/login", notice: "Anda harus login untuk mengakses halaman ini."
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
18
35
|
def stylesheet_tag(filename)
|
|
19
36
|
"<link rel='stylesheet' href='/css/#{filename}.css'>"
|
|
20
37
|
end
|
|
@@ -38,22 +55,31 @@ module Eksa
|
|
|
38
55
|
variables.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
39
56
|
|
|
40
57
|
content_path = File.expand_path("./app/views/#{template_name}.html.erb")
|
|
58
|
+
internal_content_path = File.expand_path("../../eksa/views/#{template_name}.html.erb", __FILE__)
|
|
59
|
+
|
|
41
60
|
layout_path = File.expand_path("./app/views/layout.html.erb")
|
|
61
|
+
internal_layout_path = File.expand_path("../../eksa/views/layout.html.erb", __FILE__)
|
|
62
|
+
|
|
63
|
+
actual_content_path = File.exist?(content_path) ? content_path : internal_content_path
|
|
64
|
+
actual_layout_path = File.exist?(layout_path) ? layout_path : internal_layout_path
|
|
42
65
|
|
|
43
|
-
if File.exist?(
|
|
44
|
-
@content = ERB.new(File.read(
|
|
45
|
-
if File.exist?(
|
|
46
|
-
ERB.new(File.read(
|
|
66
|
+
if File.exist?(actual_content_path)
|
|
67
|
+
@content = ERB.new(File.read(actual_content_path)).result(binding)
|
|
68
|
+
if File.exist?(actual_layout_path)
|
|
69
|
+
ERB.new(File.read(actual_layout_path)).result(binding)
|
|
47
70
|
else
|
|
48
71
|
@content
|
|
49
72
|
end
|
|
50
73
|
else
|
|
51
74
|
"<div class='glass' style='padding: 2rem; border-radius: 1rem; color: #ff5555; background: rgba(255,0,0,0.1); backdrop-filter: blur(10px);'>
|
|
52
75
|
<h2 style='margin-top:0;'>⚠️ View Error</h2>
|
|
53
|
-
<p>Template <strong>#{template_name}</strong> tidak ditemukan di
|
|
54
|
-
<code style='display:block; background:rgba(0,0,0,0.2); padding:0.5rem; border-radius:0.5rem;'>#{content_path}</code>
|
|
76
|
+
<p>Template <strong>#{template_name}</strong> tidak ditemukan di app/views atau internal eksa/views.</p>
|
|
55
77
|
</div>"
|
|
56
78
|
end
|
|
57
79
|
end
|
|
80
|
+
|
|
81
|
+
def render_internal(template_name, variables = {})
|
|
82
|
+
render(template_name, variables)
|
|
83
|
+
end
|
|
58
84
|
end
|
|
59
85
|
end
|
data/lib/eksa/markdown_post.rb
CHANGED
|
@@ -10,16 +10,19 @@ module Eksa
|
|
|
10
10
|
|
|
11
11
|
POSTS_DIR = "_posts"
|
|
12
12
|
|
|
13
|
-
def self.all
|
|
13
|
+
def self.all(include_unpublished: false)
|
|
14
14
|
return [] unless Dir.exist?(POSTS_DIR)
|
|
15
15
|
|
|
16
|
-
Dir.glob(File.join(POSTS_DIR, "*.md")).map do |file|
|
|
16
|
+
posts = Dir.glob(File.join(POSTS_DIR, "*.md")).map do |file|
|
|
17
17
|
new(file)
|
|
18
|
-
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
posts.reject! { |p| !p.published? } unless include_unpublished
|
|
21
|
+
posts.sort_by { |p| p.date }.reverse
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def self.find(slug)
|
|
22
|
-
all.find { |p| p.slug == slug }
|
|
25
|
+
all(include_unpublished: true).find { |p| p.slug == slug && p.published? }
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def initialize(file_path)
|
|
@@ -48,6 +51,10 @@ module Eksa
|
|
|
48
51
|
@metadata['image'] || ""
|
|
49
52
|
end
|
|
50
53
|
|
|
54
|
+
def published?
|
|
55
|
+
@metadata.key?('published') ? @metadata['published'] : true
|
|
56
|
+
end
|
|
57
|
+
|
|
51
58
|
def body_html
|
|
52
59
|
Kramdown::Document.new(@content, input: 'GFM').to_html
|
|
53
60
|
end
|
data/lib/eksa/user.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Eksa
|
|
2
|
+
class User < Eksa::Model
|
|
3
|
+
require 'bcrypt'
|
|
4
|
+
|
|
5
|
+
def self.setup_schema
|
|
6
|
+
db.execute <<~SQL
|
|
7
|
+
CREATE TABLE IF NOT EXISTS eksa_users (
|
|
8
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
|
+
username TEXT UNIQUE,
|
|
10
|
+
password_hash TEXT,
|
|
11
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
12
|
+
)
|
|
13
|
+
SQL
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.all
|
|
17
|
+
db.execute("SELECT id, username FROM eksa_users")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.create(username, password)
|
|
21
|
+
hash = BCrypt::Password.create(password)
|
|
22
|
+
db.execute("INSERT INTO eksa_users (username, password_hash) VALUES (?, ?)", [username, hash])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.update_password(username, new_password)
|
|
26
|
+
hash = BCrypt::Password.create(new_password)
|
|
27
|
+
db.execute("UPDATE eksa_users SET password_hash = ? WHERE username = ?", [hash, username])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.authenticate(username, password)
|
|
31
|
+
user_data = db.execute("SELECT id, username, password_hash FROM eksa_users WHERE username = ? LIMIT 1", [username]).first
|
|
32
|
+
return nil unless user_data
|
|
33
|
+
|
|
34
|
+
stored_hash = BCrypt::Password.new(user_data[2])
|
|
35
|
+
if stored_hash == password
|
|
36
|
+
{ id: user_data[0], username: user_data[1] }
|
|
37
|
+
else
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.find(id)
|
|
43
|
+
user_data = db.execute("SELECT id, username FROM eksa_users WHERE id = ? LIMIT 1", [id]).first
|
|
44
|
+
user_data ? { id: user_data[0], username: user_data[1] } : nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/eksa/version.rb
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<div class="glass max-w-md mx-auto rounded-3xl p-8 animate__animated animate__fadeIn relative overflow-hidden">
|
|
2
|
+
<div class="absolute inset-0 bg-indigo-500/10 blur-3xl rounded-full translate-x-1/2 -translate-y-1/2"></div>
|
|
3
|
+
|
|
4
|
+
<div class="relative z-10 text-center mb-8">
|
|
5
|
+
<div class="w-16 h-16 bg-white/10 rounded-2xl flex items-center justify-center mx-auto mb-4 border border-white/20 shadow-xl">
|
|
6
|
+
<i data-lucide="lock" class="w-8 h-8 text-indigo-300"></i>
|
|
7
|
+
</div>
|
|
8
|
+
<h1 class="text-3xl font-extrabold text-white tracking-tight mb-2">Masuk CMS</h1>
|
|
9
|
+
<p class="text-white/50 text-sm">Masuk untuk mengelola postingan blog Anda.</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<% if @flash && @flash[:notice] %>
|
|
13
|
+
<div class="relative z-10 mb-6 p-4 rounded-xl bg-red-500/20 border border-red-500/30 text-red-200 text-sm text-center font-medium animate__animated animate__shakeX">
|
|
14
|
+
<i data-lucide="alert-circle" class="w-4 h-4 inline-block mr-1 -mt-0.5"></i>
|
|
15
|
+
<%= @flash[:notice] %>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<form action="/auth/process_login" method="post" class="relative z-10 space-y-4">
|
|
20
|
+
<div>
|
|
21
|
+
<label class="block text-xs font-bold text-indigo-300 uppercase tracking-widest mb-2">Username</label>
|
|
22
|
+
<input type="text" name="username" required
|
|
23
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition placeholder-white/20"
|
|
24
|
+
placeholder="Masukkan username...">
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div>
|
|
28
|
+
<label class="block text-xs font-bold text-indigo-300 uppercase tracking-widest mb-2">Password</label>
|
|
29
|
+
<input type="password" name="password" required
|
|
30
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition placeholder-white/20"
|
|
31
|
+
placeholder="••••••••">
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<button type="submit" class="w-full mt-4 bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 px-4 rounded-xl transition duration-300 shadow-lg shadow-indigo-600/30 flex items-center justify-center gap-2">
|
|
35
|
+
<i data-lucide="log-in" class="w-4 h-4"></i> Sign In
|
|
36
|
+
</button>
|
|
37
|
+
</form>
|
|
38
|
+
|
|
39
|
+
<div class="relative z-10 mt-6 text-center">
|
|
40
|
+
<p class="text-white/40 text-xs">Belum punya akun admin?
|
|
41
|
+
<a href="/auth/register" class="text-indigo-400 hover:text-white transition font-medium">Register di sini</a>
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div class="glass max-w-md mx-auto rounded-3xl p-8 animate__animated animate__fadeIn relative overflow-hidden">
|
|
2
|
+
<div class="absolute inset-0 bg-emerald-500/10 blur-3xl rounded-full translate-x-1/2 -translate-y-1/2"></div>
|
|
3
|
+
|
|
4
|
+
<div class="relative z-10 text-center mb-8">
|
|
5
|
+
<div class="w-16 h-16 bg-white/10 rounded-2xl flex items-center justify-center mx-auto mb-4 border border-white/20 shadow-xl">
|
|
6
|
+
<i data-lucide="user-plus" class="w-8 h-8 text-emerald-300"></i>
|
|
7
|
+
</div>
|
|
8
|
+
<h1 class="text-3xl font-extrabold text-white tracking-tight mb-2">Registrasi Admin</h1>
|
|
9
|
+
<p class="text-white/50 text-sm">Buat akun untuk mengelola Eksa Framework.</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<% if @admin_exists %>
|
|
13
|
+
<div class="relative z-10 text-center bg-emerald-500/20 border border-emerald-500/30 rounded-xl p-6 text-emerald-200">
|
|
14
|
+
<i data-lucide="badge-check" class="w-12 h-12 mx-auto mb-3 text-emerald-400"></i>
|
|
15
|
+
<h2 class="text-lg font-bold mb-2 text-white">Akun Admin Sudah Dibuat</h2>
|
|
16
|
+
<p class="text-sm opacity-80 text-white/70">Hanya satu akun admin yang diizinkan untuk keamanan CMS.</p>
|
|
17
|
+
</div>
|
|
18
|
+
<% else %>
|
|
19
|
+
|
|
20
|
+
<% if @flash && @flash[:notice] %>
|
|
21
|
+
<div class="relative z-10 mb-6 p-4 rounded-xl bg-red-500/20 border border-red-500/30 text-red-200 text-sm text-center font-medium animate__animated animate__shakeX">
|
|
22
|
+
<i data-lucide="alert-circle" class="w-4 h-4 inline-block mr-1 -mt-0.5"></i>
|
|
23
|
+
<%= @flash[:notice] %>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<form action="/auth/process_register" method="post" class="relative z-10 space-y-4">
|
|
28
|
+
<div>
|
|
29
|
+
<label class="block text-xs font-bold text-emerald-300 uppercase tracking-widest mb-2">Username</label>
|
|
30
|
+
<input type="text" name="username" required
|
|
31
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition placeholder-white/20"
|
|
32
|
+
placeholder="Pilih username...">
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div>
|
|
36
|
+
<label class="block text-xs font-bold text-emerald-300 uppercase tracking-widest mb-2">Password</label>
|
|
37
|
+
<input type="password" name="password" required minlength="6"
|
|
38
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition placeholder-white/20"
|
|
39
|
+
placeholder="Minimal 6 karakter">
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<button type="submit" class="w-full mt-4 bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-3 px-4 rounded-xl transition duration-300 shadow-lg shadow-emerald-600/30 flex items-center justify-center gap-2">
|
|
43
|
+
<i data-lucide="shield-check" class="w-4 h-4"></i> Buat Akun
|
|
44
|
+
</button>
|
|
45
|
+
</form>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<div class="relative z-10 mt-6 text-center">
|
|
49
|
+
<p class="text-white/40 text-xs">Sudah punya akun?
|
|
50
|
+
<a href="/auth/login" class="text-emerald-400 hover:text-white transition font-medium">Login sekarang</a>
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<div class="glass max-w-4xl mx-auto rounded-3xl p-10 text-white animate__animated animate__zoomIn animate__faster relative overflow-hidden">
|
|
2
|
+
<div class="absolute inset-0 bg-yellow-500/10 blur-3xl rounded-full translate-x-1/2 -translate-y-1/2"></div>
|
|
3
|
+
|
|
4
|
+
<div class="relative z-10 flex items-center justify-between mb-8 pb-6 border-b border-white/10">
|
|
5
|
+
<div class="flex items-center gap-4">
|
|
6
|
+
<div class="w-14 h-14 bg-yellow-500/20 rounded-2xl flex items-center justify-center border border-yellow-500/30 shadow-xl shadow-yellow-500/20">
|
|
7
|
+
<i data-lucide="edit-3" class="w-7 h-7 text-yellow-300"></i>
|
|
8
|
+
</div>
|
|
9
|
+
<div>
|
|
10
|
+
<h1 class="text-3xl font-extrabold tracking-tight">Edit <span class="text-yellow-300">Post</span></h1>
|
|
11
|
+
<p class="text-white/50 text-sm mt-1">Mengedit <%= @post.filename %></p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<a href="/cms" class="px-4 py-2 bg-white/5 hover:bg-white/10 rounded-xl transition border border-white/10 text-sm font-medium flex items-center gap-2">
|
|
15
|
+
<i data-lucide="arrow-left" class="w-4 h-4"></i> Kembali
|
|
16
|
+
</a>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<form action="/cms/update/<%= @post.slug %>" method="post" class="relative z-10 space-y-6">
|
|
20
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
21
|
+
<div class="space-y-2">
|
|
22
|
+
<label class="block text-xs font-bold text-yellow-300 uppercase tracking-widest">Judul Postingan</label>
|
|
23
|
+
<input type="text" name="title" value="<%= @post.title %>" required
|
|
24
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 transition">
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="space-y-2">
|
|
28
|
+
<label class="block text-xs font-bold text-yellow-300 uppercase tracking-widest">Kategori</label>
|
|
29
|
+
<input type="text" name="category" value="<%= @post.category %>" required
|
|
30
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 transition">
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="space-y-2">
|
|
34
|
+
<label class="block text-xs font-bold text-yellow-300 uppercase tracking-widest">Penulis (Author)</label>
|
|
35
|
+
<input type="text" name="author" value="<%= @post.author %>" required
|
|
36
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 transition">
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="space-y-2">
|
|
40
|
+
<label class="block text-xs font-bold text-yellow-300 uppercase tracking-widest">Image Thumbnail URL</label>
|
|
41
|
+
<input type="text" name="image" value="<%= @post.image %>"
|
|
42
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 transition"
|
|
43
|
+
placeholder="Contoh: header.jpg atau kosongkan">
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="space-y-2 pt-4">
|
|
48
|
+
<label class="block text-xs font-bold text-yellow-300 uppercase tracking-widest">Konten (Markdown)</label>
|
|
49
|
+
<div class="relative group">
|
|
50
|
+
<textarea name="content" required rows="15"
|
|
51
|
+
class="w-full bg-black/40 border border-white/10 rounded-xl px-5 py-4 text-white focus:outline-none focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500 font-mono text-sm leading-relaxed transition"><%= @post.content.strip %></textarea>
|
|
52
|
+
<!-- Decorative subtle icon -->
|
|
53
|
+
<i data-lucide="file-text" class="absolute bottom-4 right-4 w-12 h-12 text-white/5 pointer-events-none transition group-hover:text-yellow-500/10"></i>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="pt-6 border-t border-white/5 flex justify-end">
|
|
58
|
+
<button type="submit" class="bg-yellow-500 hover:bg-yellow-400 text-yellow-950 font-bold py-3 px-8 rounded-xl transition duration-300 shadow-xl shadow-yellow-500/20 flex items-center gap-2">
|
|
59
|
+
<i data-lucide="save" class="w-5 h-5"></i> Simpan Perubahan
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</form>
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<div class="glass rounded-3xl p-10 text-white animate__animated animate__fadeIn">
|
|
2
|
+
<div class="flex items-center justify-between mb-10 pb-6 border-b border-white/10">
|
|
3
|
+
<div class="flex items-center gap-4">
|
|
4
|
+
<div class="p-4 bg-indigo-500 rounded-2xl shadow-xl">
|
|
5
|
+
<i data-lucide="layout-dashboard" class="w-8 h-8"></i>
|
|
6
|
+
</div>
|
|
7
|
+
<div>
|
|
8
|
+
<h1 class="text-4xl font-extrabold tracking-tight">CMS <span class="text-indigo-300">Dashboard</span></h1>
|
|
9
|
+
<p class="text-white/50">Kelola visibilitas postingan blog Anda</p>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="flex items-center gap-4">
|
|
13
|
+
<span class="px-4 py-2 bg-white/5 rounded-xl border border-white/10 text-sm font-medium text-white/80">
|
|
14
|
+
<i data-lucide="user" class="w-4 h-4 inline-block mr-1"></i> Admin
|
|
15
|
+
</span>
|
|
16
|
+
<a href="/auth/logout" class="px-4 py-2 bg-red-500/20 hover:bg-red-500/40 text-red-300 rounded-xl border border-red-500/30 transition shadow-lg text-sm font-bold flex items-center gap-2">
|
|
17
|
+
<i data-lucide="log-out" class="w-4 h-4"></i> Logout
|
|
18
|
+
</a>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="overflow-hidden rounded-2xl border border-white/10 bg-black/20">
|
|
23
|
+
<table class="w-full text-left border-collapse">
|
|
24
|
+
<thead>
|
|
25
|
+
<tr class="bg-white/5 text-indigo-200 text-xs uppercase tracking-widest">
|
|
26
|
+
<th class="p-4 font-bold border-b border-white/10">Postingan</th>
|
|
27
|
+
<th class="p-4 font-bold border-b border-white/10">Kategori</th>
|
|
28
|
+
<th class="p-4 font-bold border-b border-white/10">Status</th>
|
|
29
|
+
<th class="p-4 font-bold border-b border-white/10 text-right">Aksi</th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody class="divide-y divide-white/5">
|
|
33
|
+
<% if @posts.empty? %>
|
|
34
|
+
<tr>
|
|
35
|
+
<td colspan="4" class="p-8 text-center text-white/40">
|
|
36
|
+
<i data-lucide="folder-open" class="w-8 h-8 mx-auto mb-2 opacity-50"></i>
|
|
37
|
+
Belum ada postingan markdown di _posts/
|
|
38
|
+
</td>
|
|
39
|
+
</tr>
|
|
40
|
+
<% else %>
|
|
41
|
+
<% @posts.each do |post| %>
|
|
42
|
+
<tr class="hover:bg-white/5 transition">
|
|
43
|
+
<td class="p-4">
|
|
44
|
+
<p class="font-bold text-lg"><%= post.title %></p>
|
|
45
|
+
<p class="text-xs text-white/40 font-mono"><%= post.filename %></p>
|
|
46
|
+
</td>
|
|
47
|
+
<td class="p-4">
|
|
48
|
+
<span class="text-[10px] font-bold text-indigo-300 uppercase tracking-widest px-2 py-1 bg-indigo-500/20 rounded-md border border-indigo-500/30">
|
|
49
|
+
<%= post.category %>
|
|
50
|
+
</span>
|
|
51
|
+
</td>
|
|
52
|
+
<td class="p-4">
|
|
53
|
+
<% if post.published? %>
|
|
54
|
+
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">
|
|
55
|
+
<span class="w-2 h-2 rounded-full bg-emerald-400"></span> Aktif
|
|
56
|
+
</span>
|
|
57
|
+
<% else %>
|
|
58
|
+
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-gray-500/20 text-gray-400 border border-gray-500/30">
|
|
59
|
+
<span class="w-2 h-2 rounded-full bg-gray-400"></span> Draft
|
|
60
|
+
</span>
|
|
61
|
+
<% end %>
|
|
62
|
+
</td>
|
|
63
|
+
<td class="p-4 text-right space-x-2">
|
|
64
|
+
<a href="/cms/edit/<%= post.slug %>" class="inline-flex items-center justify-center p-2 rounded-lg bg-yellow-500/20 hover:bg-yellow-500/40 text-yellow-300 transition border border-yellow-500/30" title="Edit Post">
|
|
65
|
+
<i data-lucide="edit" class="w-4 h-4"></i>
|
|
66
|
+
</a>
|
|
67
|
+
<a href="/posts/<%= post.slug %>" target="_blank" class="inline-flex items-center justify-center p-2 rounded-lg bg-white/5 hover:bg-white/10 text-white/70 transition border border-white/10" title="Lihat Post">
|
|
68
|
+
<i data-lucide="external-link" class="w-4 h-4"></i>
|
|
69
|
+
</a>
|
|
70
|
+
<a href="/cms/toggle/<%= post.slug %>" class="inline-flex items-center justify-center p-2 rounded-lg bg-indigo-500/20 hover:bg-indigo-500/40 text-indigo-300 transition border border-indigo-500/30" title="Toggle Status">
|
|
71
|
+
<i data-lucide="<%= post.published? ? 'eye-off' : 'eye' %>" class="w-4 h-4"></i>
|
|
72
|
+
</a>
|
|
73
|
+
<a href="/cms/delete/<%= post.slug %>" onclick="return confirm('Yakin ingin menghapus postingan ini selamanya?')" class="inline-flex items-center justify-center p-2 rounded-lg bg-red-500/20 hover:bg-red-500/40 text-red-300 transition border border-red-500/30" title="Hapus Permanen">
|
|
74
|
+
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
75
|
+
</a>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<% end %>
|
|
79
|
+
<% end %>
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
data/lib/eksa.rb
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
require 'rack'
|
|
2
|
+
require 'rack/session'
|
|
2
3
|
require 'json'
|
|
3
4
|
require_relative 'eksa/version'
|
|
4
5
|
require_relative 'eksa/controller'
|
|
5
6
|
require_relative 'eksa/model'
|
|
6
7
|
require_relative 'eksa/markdown_post'
|
|
8
|
+
require_relative 'eksa/user'
|
|
9
|
+
require_relative 'eksa/auth_controller'
|
|
10
|
+
require_relative 'eksa/cms_controller'
|
|
7
11
|
|
|
8
12
|
module Eksa
|
|
9
13
|
class Application
|
|
10
|
-
attr_reader :config, :middlewares
|
|
14
|
+
attr_reader :config, :middlewares, :features
|
|
11
15
|
|
|
12
16
|
def initialize
|
|
13
17
|
@routes = {}
|
|
14
18
|
@middlewares = []
|
|
19
|
+
@features = load_feature_flags
|
|
15
20
|
@config = {
|
|
16
|
-
db_path: File.expand_path("./db/eksa_app.db")
|
|
21
|
+
db_path: File.expand_path("./db/eksa_app.db"),
|
|
22
|
+
session_secret: ENV['SESSION_SECRET'] || 'eksa_super_secret_key_change_me_in_production_make_it_sixty_four_bytes_or_more'
|
|
17
23
|
}
|
|
18
24
|
yield self if block_given?
|
|
19
25
|
configure_framework
|
|
@@ -21,6 +27,40 @@ module Eksa
|
|
|
21
27
|
|
|
22
28
|
def configure_framework
|
|
23
29
|
Eksa::Model.database_path = @config[:db_path]
|
|
30
|
+
|
|
31
|
+
# Setup Session Middleware for Authentication
|
|
32
|
+
use Rack::Session::Cookie, secret: @config[:session_secret], key: 'eksa.session'
|
|
33
|
+
|
|
34
|
+
auto_mount_features
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load_feature_flags
|
|
38
|
+
config_path = File.expand_path('./.eksa.json')
|
|
39
|
+
if File.exist?(config_path)
|
|
40
|
+
JSON.parse(File.read(config_path))
|
|
41
|
+
else
|
|
42
|
+
{ 'cms' => false, 'auth' => false }
|
|
43
|
+
end
|
|
44
|
+
rescue JSON::ParserError
|
|
45
|
+
{ 'cms' => false, 'auth' => false }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def auto_mount_features
|
|
49
|
+
if @features['auth']
|
|
50
|
+
add_route "/auth/login", Eksa::AuthController, :login
|
|
51
|
+
add_route "/auth/register", Eksa::AuthController, :register
|
|
52
|
+
add_route "/auth/logout", Eksa::AuthController, :logout
|
|
53
|
+
add_route "/auth/process_login", Eksa::AuthController, :process_login
|
|
54
|
+
add_route "/auth/process_register", Eksa::AuthController, :process_register
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if @features['cms']
|
|
58
|
+
add_route "/cms", Eksa::CmsController, :index
|
|
59
|
+
add_route "/cms/edit/:slug", Eksa::CmsController, :edit
|
|
60
|
+
add_route "/cms/update/:slug", Eksa::CmsController, :update_post
|
|
61
|
+
add_route "/cms/toggle/:slug", Eksa::CmsController, :toggle_status
|
|
62
|
+
add_route "/cms/delete/:slug", Eksa::CmsController, :delete_post
|
|
63
|
+
end
|
|
24
64
|
end
|
|
25
65
|
|
|
26
66
|
def add_route(path, controller_class, action)
|
|
@@ -82,6 +122,9 @@ module Eksa
|
|
|
82
122
|
else
|
|
83
123
|
response = Rack::Response.new
|
|
84
124
|
if controller_instance.status == 302
|
|
125
|
+
if controller_instance.flash[:notice] && !controller_instance.flash[:notice].empty?
|
|
126
|
+
response.set_cookie('eksa_flash', value: controller_instance.flash[:notice], path: '/')
|
|
127
|
+
end
|
|
85
128
|
response.redirect(controller_instance.redirect_url, 302)
|
|
86
129
|
else
|
|
87
130
|
response.write(response_data)
|
|
@@ -89,7 +132,9 @@ module Eksa
|
|
|
89
132
|
end
|
|
90
133
|
end
|
|
91
134
|
|
|
92
|
-
|
|
135
|
+
if flash_message && controller_instance.status != 302
|
|
136
|
+
response.delete_cookie('eksa_flash', path: '/')
|
|
137
|
+
end
|
|
93
138
|
response.finish
|
|
94
139
|
else
|
|
95
140
|
html = <<~HTML
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eksa-framework
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- IshikawaUta
|
|
@@ -93,6 +93,34 @@ dependencies:
|
|
|
93
93
|
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '1.1'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: bcrypt
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '3.1'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.1'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rack-session
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '2.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '2.0'
|
|
96
124
|
description: Framework MVC ringan dengan tema modern, sistem routing, dan auto-database
|
|
97
125
|
SQLite.
|
|
98
126
|
email:
|
|
@@ -102,6 +130,7 @@ executables:
|
|
|
102
130
|
extensions: []
|
|
103
131
|
extra_rdoc_files: []
|
|
104
132
|
files:
|
|
133
|
+
- ".eksa.json"
|
|
105
134
|
- Gemfile
|
|
106
135
|
- Gemfile.lock
|
|
107
136
|
- LICENSE
|
|
@@ -123,10 +152,17 @@ files:
|
|
|
123
152
|
- db/setup.rb
|
|
124
153
|
- exe/eksa
|
|
125
154
|
- lib/eksa.rb
|
|
155
|
+
- lib/eksa/auth_controller.rb
|
|
156
|
+
- lib/eksa/cms_controller.rb
|
|
126
157
|
- lib/eksa/controller.rb
|
|
127
158
|
- lib/eksa/markdown_post.rb
|
|
128
159
|
- lib/eksa/model.rb
|
|
160
|
+
- lib/eksa/user.rb
|
|
129
161
|
- lib/eksa/version.rb
|
|
162
|
+
- lib/eksa/views/auth/login.html.erb
|
|
163
|
+
- lib/eksa/views/auth/register.html.erb
|
|
164
|
+
- lib/eksa/views/cms/edit.html.erb
|
|
165
|
+
- lib/eksa/views/cms/index.html.erb
|
|
130
166
|
- public/css/style.css
|
|
131
167
|
- public/img/favicon.ico
|
|
132
168
|
- public/img/logo.jpg
|