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.
@@ -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
@@ -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?(content_path)
44
- @content = ERB.new(File.read(content_path)).result(binding)
45
- if File.exist?(layout_path)
46
- ERB.new(File.read(layout_path)).result(binding)
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:</p>
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
@@ -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.sort_by { |p| p.date }.reverse
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
@@ -1,3 +1,3 @@
1
1
  module Eksa
2
- VERSION = "3.3.3"
2
+ VERSION = "3.4.3"
3
3
  end
@@ -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
- response.delete_cookie('eksa_flash') if flash_message
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.3.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