eksa-framework 3.3.3 → 3.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.
@@ -6,7 +6,7 @@
6
6
  </div>
7
7
  <h1 class="text-4xl font-extrabold tracking-tight">Dokumentasi <span class="text-indigo-300">Eksa</span></h1>
8
8
  </div>
9
- <p class="text-white/60 text-lg">Panduan profesional instalasi dan pengembangan aplikasi menggunakan Eksa Framework.</p>
9
+ <p class="text-white/60 text-lg">Panduan profesional instalasi dan pengembangan aplikasi menggunakan Eksa Framework v3.5.0.</p>
10
10
  </div>
11
11
 
12
12
  <div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
@@ -14,6 +14,7 @@
14
14
  <h3 class="font-bold text-indigo-300 uppercase text-xs tracking-widest">Memulai</h3>
15
15
  <ul class="space-y-2 text-sm">
16
16
  <li><a href="#instalasi" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="download" class="w-3 h-3"></i> Instalasi Gem</a></li>
17
+ <li><a href="#keamanan" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="shield" class="w-3 h-3"></i> Setup Keamanan</a></li>
17
18
  <li><a href="#struktur" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="folder" class="w-3 h-3"></i> Struktur Project</a></li>
18
19
  </ul>
19
20
  </div>
@@ -21,15 +22,16 @@
21
22
  <h3 class="font-bold text-indigo-300 uppercase text-xs tracking-widest">Pengembangan</h3>
22
23
  <ul class="space-y-2 text-sm">
23
24
  <li><a href="#routing" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="git-branch" class="w-3 h-3"></i> Routing System</a></li>
24
- <li><a href="#build" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="package" class="w-3 h-3"></i> Build Framework</a></li>
25
+ <li><a href="#database" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="database" class="w-3 h-3"></i> Agnostic Database</a></li>
26
+ <li><a href="#jit" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="zap" class="w-3 h-3"></i> JIT Schema</a></li>
25
27
  </ul>
26
28
  </div>
27
29
  <div class="space-y-4">
28
30
  <h3 class="font-bold text-indigo-300 uppercase text-xs tracking-widest">Fitur Core</h3>
29
31
  <ul class="space-y-2 text-sm">
30
- <li><a href="#database" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="database" class="w-3 h-3"></i> Auto-Migration</a></li>
31
- <li><a href="#flash" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="bell" class="w-3 h-3"></i> Flash UI</a></li>
32
32
  <li><a href="#blog" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="pen-tool" class="w-3 h-3"></i> Markdown Blog</a></li>
33
+ <li><a href="#cms" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="layout-dashboard" class="w-3 h-3"></i> CMS & Auth</a></li>
34
+ <li><a href="#seo" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="search" class="w-3 h-3"></i> SEO & JSON-LD</a></li>
33
35
  </ul>
34
36
  </div>
35
37
  </div>
@@ -48,123 +50,99 @@
48
50
  <p class="text-indigo-300">eksa init</p>
49
51
  </div>
50
52
  <div>
51
- <p class="text-white/30"># 3. Install dependensi</p>
52
- <p class="text-indigo-300">bundle install</p>
53
- </div>
54
- <div>
55
- <p class="text-white/30"># 4. Jalankan server</p>
56
- <p class="text-indigo-300">eksa run</p>
53
+ <p class="text-white/30"># 3. Jalankan server</p>
54
+ <p class="text-indigo-300">bundle install && eksa run</p>
57
55
  </div>
58
56
  </div>
59
57
  </section>
60
58
 
61
- <section id="struktur" class="mb-12 scroll-mt-24">
59
+ <section id="keamanan" class="mb-12 scroll-mt-24">
62
60
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
63
- <i data-lucide="folder-tree" class="w-6 h-6 text-indigo-400"></i> Struktur Project
61
+ <i data-lucide="shield-check" class="w-6 h-6 text-indigo-400"></i> Setup Keamanan (.env)
64
62
  </h2>
65
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
66
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
67
- <code class="text-indigo-300 font-bold">app/</code>
68
- <p class="text-xs text-white/50 mt-1">Berisi Controllers, Models, dan Views aplikasi Anda.</p>
69
- </div>
70
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
71
- <code class="text-indigo-300 font-bold">db/</code>
72
- <p class="text-xs text-white/50 mt-1">Lokasi penyimpanan database SQLite (Terpisah dari app).</p>
73
- </div>
74
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
75
- <code class="text-indigo-300 font-bold">lib/eksa/</code>
76
- <p class="text-xs text-white/50 mt-1">Mesin inti (Core Engine) dari Eksa Framework.</p>
77
- </div>
78
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
79
- <code class="text-indigo-300 font-bold">spec/</code>
80
- <p class="text-xs text-white/50 mt-1">Folder testing otomatis menggunakan RSpec (disertakan saat <code class="text-indigo-200">init</code>).</p>
81
- </div>
82
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
83
- <code class="text-indigo-300 font-bold">_posts/</code>
84
- <p class="text-xs text-white/50 mt-1">Pusat file markdown untuk konten blog aplikasi.</p>
85
- </div>
86
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
87
- <code class="text-indigo-300 font-bold">exe/</code>
88
- <p class="text-xs text-white/50 mt-1">Executable files seperti CLI <code class="text-indigo-200">eksa</code>.</p>
89
- </div>
63
+ <p class="text-white/60 mb-4 text-sm">Gunakan file <code class="text-indigo-200">.env</code> untuk menyimpan kredensial sensitif seperti URI MongoDB Atlas dan Secret Session.</p>
64
+ <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10 space-y-2">
65
+ <p class="text-white/30"># Salin template example</p>
66
+ <p class="text-indigo-300">cp .env.example .env</p>
67
+ <p class="text-white/30 mt-4"># Isi EKSA_MONGODB_URI di dalam .env</p>
68
+ <p class="text-indigo-300">EKSA_MONGODB_URI="mongodb+srv://..."</p>
90
69
  </div>
91
70
  </section>
92
71
 
93
- <section id="routing" class="mb-12 scroll-mt-24">
72
+ <section id="database" class="mb-12 scroll-mt-24">
94
73
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
95
- <i data-lucide="git-branch" class="w-6 h-6 text-indigo-400"></i> Routing System
74
+ <i data-lucide="database" class="w-6 h-6 text-indigo-400"></i> Agnostic Database Engine
96
75
  </h2>
97
- <p class="text-white/60 mb-4 text-sm">Definisikan rute aplikasi Anda di dalam file <code class="text-indigo-200">config.ru</code>. Eksa mendukung pemetaan URL ke Controller dan Action secara eksplisit.</p>
98
- <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
99
- <p class="text-white/30"># app.add_route(path, controller_class, action_symbol)</p>
100
- <p class="text-indigo-300 italic">app.add_route "/", PagesController, :index</p>
101
- <p class="text-indigo-300 italic">app.add_route "/about", PagesController, :about</p>
76
+ <p class="text-white/60 mb-4 text-sm">Pindah dari SQLite lokal ke cloud MongoDB Atlas secara instan melalui CLI.</p>
77
+ <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10 space-y-4">
78
+ <div>
79
+ <p class="text-white/30"># Ganti tipe database</p>
80
+ <p class="text-indigo-300">eksa db switch mongo</p>
81
+ </div>
82
+ <div>
83
+ <p class="text-white/30"># Migrasi data otomatis (SQLite -> Mongo)</p>
84
+ <p class="text-indigo-300">eksa db migrate --from sqlite --to mongo</p>
85
+ </div>
102
86
  </div>
103
87
  </section>
104
88
 
105
- <section id="database" class="mb-12 scroll-mt-24">
89
+ <section id="jit" class="mb-12 scroll-mt-24">
106
90
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
107
- <i data-lucide="database" class="w-6 h-6 text-indigo-400"></i> Auto-Migration
91
+ <i data-lucide="zap" class="w-6 h-6 text-indigo-400"></i> Just-In-Time Schema Initialization
108
92
  </h2>
109
- <p class="text-white/60 mb-4 text-sm">Eksa secara otomatis membuat file database dan tabel saat aplikasi pertama kali dijalankan melalui <code class="text-indigo-200">Eksa::Model</code>.</p>
93
+ <p class="text-white/60 mb-4 text-sm">Anda tidak perlu lagi menjalankan migrasi manual. Eksa akan mendeteksi dan membuat tabel/koleksi secara otomatis tepat saat model pertama kali diakses.</p>
110
94
  <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
111
- <p class="text-indigo-300">def self.setup_initial_schema</p>
112
- <p class="text-white/70 ml-4">@db.execute "CREATE TABLE IF NOT EXISTS pesan (...)"</p>
113
- <p class="text-indigo-300">end</p>
95
+ <p class="text-white/30"># Definisikan saja di model</p>
96
+ <p class="text-indigo-300">class Post < Eksa::Model</p>
97
+ <p class="text-indigo-300 italic ml-4">def self.setup_schema</p>
98
+ <p class="text-indigo-300 italic ml-8">db.execute "CREATE TABLE IF NOT EXISTS ..."</p>
99
+ <p class="text-indigo-300 italic ml-4">end</p>
100
+ <p class="text-indigo-300">main</p>
114
101
  </div>
115
- <p class="text-xs text-white/40 mt-3 italic">* Database tersimpan secara persisten di folder /db/eksa_app.db</p>
116
102
  </section>
117
103
 
118
- <section id="flash" class="mb-12 scroll-mt-24">
104
+ <section id="blog" class="mb-12 scroll-mt-24">
119
105
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
120
- <i data-lucide="bell" class="w-6 h-6 text-indigo-400"></i> Flash UI Notification
106
+ <i data-lucide="pen-tool" class="w-6 h-6 text-indigo-400"></i> Markdown Blog Engine
121
107
  </h2>
122
- <p class="text-white/60 mb-4 text-sm">Kirim feedback instan ke user menggunakan fitur redirect dengan notice.</p>
108
+ <p class="text-white/60 mb-4 text-sm">Kelola konten blog dengan file Markdown. Mendukung Front Matter YAML, Syntax Highlighting, dan SEO otomatis.</p>
123
109
  <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
124
- <p class="text-white/30"># Di dalam Controller</p>
125
- <p class="text-indigo-300">redirect_to "/", notice: "Data berhasil disimpan!"</p>
126
- </div>
127
- <div class="mt-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-xl flex items-center gap-3">
128
- <i data-lucide="info" class="w-5 h-5 text-emerald-400"></i>
129
- <p class="text-xs text-emerald-200/80">Sistem Flash Eksa menggunakan cookie sementara yang otomatis dihapus setelah notifikasi muncul (Standar Rack 3).</p>
110
+ <p class="text-white/30"># Generate post baru</p>
111
+ <p class="text-indigo-300">eksa g post "Judul Postingan" --category "Tech" --image "url.jpg"</p>
130
112
  </div>
131
113
  </section>
132
114
 
133
- <section id="blog" class="mb-12 scroll-mt-24">
115
+ <section id="cms" class="mb-12 scroll-mt-24">
134
116
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
135
- <i data-lucide="pen-tool" class="w-6 h-6 text-indigo-400"></i> Markdown Blog Engine
117
+ <i data-lucide="layout-dashboard" class="w-6 h-6 text-indigo-400"></i> CMS Dashboard & Auth
136
118
  </h2>
137
- <p class="text-white/60 mb-4 text-sm">Kelola konten blog dengan file Markdown. Mendukung Front Matter YAML dan rute dinamis.</p>
119
+ <p class="text-white/60 mb-4 text-sm">Gunakan panel admin terintegrasi untuk mengelola postingan, status publikasi, dan keamanan akun.</p>
138
120
  <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
139
- <p class="text-white/30"># Buat post baru lewat CLI (dengan opsi meta)</p>
140
- <p class="text-indigo-300">eksa g post "Judul Postingan" --category "Tech" --author "Nama" --image "url.jpg"</p>
141
- <p class="text-white/30 mt-4"># Akses di URL (Otomatis)</p>
142
- <p class="text-white/70">/posts/:slug</p>
143
- </div>
144
- <div class="mt-4 p-4 bg-indigo-500/10 border border-indigo-500/20 rounded-xl flex items-center gap-3">
145
- <i data-lucide="zap" class="w-5 h-5 text-indigo-400"></i>
146
- <p class="text-xs text-indigo-200/80">Sudah terintegrasi dengan Prism.js untuk highlighting kode otomatis dan tombol copy!</p>
121
+ <p class="text-white/30"># Aktivasi fitur</p>
122
+ <p class="text-indigo-300">eksa feature enable auth</p>
123
+ <p class="text-indigo-300">eksa feature enable cms</p>
124
+ <p class="text-white/30 mt-4"># Akses Dashboard</p>
125
+ <p class="text-white/70">http://localhost:9292/cms</p>
147
126
  </div>
148
127
  </section>
149
128
 
150
- <section id="build" class="mb-12 scroll-mt-24">
129
+ <section id="seo" class="mb-12 scroll-mt-24">
151
130
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
152
- <i data-lucide="package-check" class="w-6 h-6 text-indigo-400"></i> Build & Publish
131
+ <i data-lucide="search" class="w-6 h-6 text-indigo-400"></i> SEO & JSON-LD (Structured Data)
153
132
  </h2>
154
- <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
155
- <p class="text-white/30"># Build gem lokal</p>
156
- <p class="text-indigo-300">gem build eksa-framework.gemspec</p>
157
- <p class="text-white/30 mt-4"># Publish ke RubyGems / GitHub Packages</p>
158
- <p class="text-indigo-300">gem push eksa-framework-3.3.3.gem</p>
133
+ <p class="text-white/60 mb-4 text-sm">Eksa v3.5.0 secara otomatis menyematkan data terstruktur untuk ranking pencarian yang lebih baik.</p>
134
+ <div class="bg-white/5 p-4 rounded-xl border border-white/10 flex items-center gap-3">
135
+ <i data-lucide="check-circle-2" class="w-5 h-5 text-emerald-400"></i>
136
+ <p class="text-xs text-white/70">Mendukung <code class="text-indigo-300">sitemap.xml</code>, <code class="text-indigo-300">robots.txt</code>, dan skema <code class="text-indigo-300">BlogPosting</code> secara dinamis.</p>
159
137
  </div>
160
138
  </section>
161
139
 
162
140
  <div class="mt-12 pt-8 border-t border-white/10 text-center">
163
141
  <div class="flex justify-center gap-4 mb-4">
164
142
  <span class="bg-indigo-500/20 text-indigo-300 text-[10px] px-3 py-1 rounded-full border border-indigo-500/30 uppercase tracking-tighter">Rack 3.0 Compatible</span>
165
- <span class="bg-emerald-500/20 text-emerald-300 text-[10px] px-3 py-1 rounded-full border border-emerald-500/30 uppercase tracking-tighter">SQLite3 Auto-Ready</span>
143
+ <span class="bg-emerald-500/20 text-emerald-300 text-[10px] px-3 py-1 rounded-full border border-emerald-500/30 uppercase tracking-tighter">Production Ready</span>
166
144
  </div>
167
- <p class="text-white/40 text-sm italic">Eksa Framework v3.3.3 Alpha Documentation</p>
145
+ <p class="text-white/40 text-sm italic">Eksa Framework v3.5.0 Professional Documentation</p>
168
146
  <a href="/" class="inline-block mt-6 text-indigo-300 hover:text-white transition font-bold flex items-center justify-center gap-2">
169
147
  <i data-lucide="arrow-left" class="w-4 h-4"></i> Kembali ke Beranda
170
148
  </a>
@@ -10,7 +10,7 @@
10
10
  </div>
11
11
 
12
12
  <p class="text-white/60 mb-8">
13
- Lakukan perubahan pada konten atau pengirim. Perubahan akan langsung diperbarui di database SQLite.
13
+ Lakukan perubahan pada konten atau pengirim. Perubahan akan langsung diperbarui di database.
14
14
  </p>
15
15
 
16
16
  <form action="/edit?id=<%= @id %>" method="post" class="space-y-6">
@@ -2,7 +2,7 @@
2
2
  <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
3
3
  <div>
4
4
  <span class="inline-block bg-indigo-500/20 text-indigo-200 text-xs font-bold px-3 py-1 rounded-full mb-3 border border-indigo-500/30">
5
- v3.3.3 Alpha
5
+ v3.5.0 Alpha
6
6
  </span>
7
7
  <h1 class="text-4xl font-extrabold tracking-tight">
8
8
  Halo, <span class="text-indigo-300"><%= @nama %></span>!
@@ -40,7 +40,7 @@
40
40
  class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition placeholder-white/20 h-32 resize-none"></textarea>
41
41
  </div>
42
42
  <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 rounded-xl transition duration-300 flex items-center justify-center gap-2 shadow-lg shadow-indigo-900/20">
43
- <i data-lucide="send" class="w-4 h-4"></i> Simpan ke SQLite
43
+ <i data-lucide="send" class="w-4 h-4"></i> Simpan ke Database
44
44
  </button>
45
45
  </form>
46
46
  </div>
data/db/eksa_app.db ADDED
Binary file
data/db/setup.rb CHANGED
@@ -1,13 +1,13 @@
1
- require 'sqlite3'
1
+ require_relative '../lib/eksa'
2
2
 
3
- db = SQLite3::Database.new "eksa_app.db"
3
+ db = Eksa::Database.adapter
4
4
 
5
5
  db.execute <<-SQL
6
6
  CREATE TABLE IF NOT EXISTS pesan (
7
- id INTEGER PRIMARY KEY,
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
8
  konten TEXT,
9
9
  pengirim TEXT
10
10
  );
11
11
  SQL
12
12
 
13
- puts "Database Eksa berhasil disiapkan!"
13
+ puts "Database Eksa berhasil disiapkan (Engine: #{Eksa::Database.adapter.class})!"
data/exe/eksa CHANGED
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'fileutils'
5
+ require 'json'
5
6
 
6
7
  def usage
7
8
  puts "\n✨ Eksa Framework CLI ✨"
@@ -11,6 +12,10 @@ def usage
11
12
  puts " eksa g controller NAME - Generate controller baru"
12
13
  puts " eksa g model NAME - Generate model baru"
13
14
  puts " eksa g post TITLE - Post baru [args: --category, --author, --image]"
15
+ puts " eksa feature ACTION F - Toggle fitur: action=enable/disable, F=cms/auth"
16
+ puts " eksa reset-password USR PWD - Reset password akun admin"
17
+ puts " eksa db switch TYPE [URI] - Ganti DB: TYPE=sqlite/mongo"
18
+ puts " eksa db migrate --from TYPE --to TYPE - Pindahkan data antar DB"
14
19
  puts " eksa run - Jalankan server aplikasi"
15
20
  puts "-----------------------------\n"
16
21
  end
@@ -21,7 +26,7 @@ def init_project
21
26
 
22
27
  puts "🚀 Menginisialisasi project di: #{target_dir}"
23
28
 
24
- items_to_init = ['app', 'lib', 'public', 'db', 'spec', '_posts', 'config.ru', 'Gemfile', 'README.md']
29
+ items_to_init = ['app', 'lib', 'public', 'db', 'spec', '_posts', 'config.ru', 'Gemfile', 'README.md', '.env.example']
25
30
 
26
31
  items_to_init.each do |item|
27
32
  source = File.join(gem_root, item)
@@ -142,6 +147,159 @@ def generate_post(title, options = {})
142
147
  puts " [OK] Created #{filename}"
143
148
  end
144
149
 
150
+ def toggle_feature(action, feature)
151
+ valid_features = ['cms', 'auth']
152
+
153
+ unless valid_features.include?(feature)
154
+ puts "❌ Error: Fitur tidak dikenali. Pilihan: #{valid_features.join(', ')}"
155
+ return
156
+ end
157
+
158
+ unless ['enable', 'disable'].include?(action)
159
+ puts "❌ Error: Aksi tidak dikenali. Gunakan 'enable' atau 'disable'."
160
+ return
161
+ end
162
+
163
+ config_path = File.join(Dir.pwd, '.eksa.json')
164
+
165
+ begin
166
+ config = File.exist?(config_path) ? JSON.parse(File.read(config_path)) : { 'cms' => false, 'auth' => false }
167
+ rescue JSON::ParserError
168
+ config = { 'cms' => false, 'auth' => false }
169
+ end
170
+
171
+ config[feature] = (action == 'enable')
172
+
173
+ File.write(config_path, JSON.pretty_generate(config))
174
+
175
+ status_color = action == 'enable' ? "✅ Aktif" : "🚫 Nonaktif"
176
+ puts "⚙️ Fitur '#{feature}' berhasil di #{status_color}."
177
+
178
+ if action == 'enable' && feature == 'cms'
179
+ puts " Tip: Pastikan Anda juga mengaktifkan 'auth' agar CMS Anda aman."
180
+ end
181
+ end
182
+
183
+ def reset_password(username, password)
184
+ require 'bcrypt'
185
+ require_relative '../lib/eksa/database'
186
+
187
+ begin
188
+ db = Eksa::Database.adapter
189
+ user = db.execute("SELECT id FROM eksa_users WHERE username = ? LIMIT 1", [username]).first
190
+ rescue => e
191
+ puts "❌ Error: Gagal mengakses database: #{e.message}"
192
+ return
193
+ end
194
+
195
+ if user
196
+ hash = BCrypt::Password.create(password)
197
+ db.execute("UPDATE eksa_users SET password_hash = ? WHERE username = ?", [hash, username])
198
+ puts "✅ Akses dipulihkan! Password untuk admin '#{username}' berhasil diubah."
199
+ else
200
+ puts "❌ Error: User '#{username}' tidak ditemukan dalam database."
201
+ end
202
+ end
203
+
204
+ def switch_database(type, uri = nil)
205
+ valid_types = ['sqlite', 'mongo', 'mongodb']
206
+ unless valid_types.include?(type.downcase)
207
+ puts "❌ Error: Tipe database tidak dikenali. Pilih: sqlite atau mongo."
208
+ return
209
+ end
210
+
211
+ config_path = File.join(Dir.pwd, '.eksa.json')
212
+ begin
213
+ config = File.exist?(config_path) ? JSON.parse(File.read(config_path)) : {}
214
+ rescue
215
+ config = {}
216
+ end
217
+
218
+ config['database'] ||= {}
219
+ config['database']['type'] = type.downcase == 'mongo' ? 'mongodb' : 'sqlite'
220
+
221
+ if type.downcase == 'mongo' || type.downcase == 'mongodb'
222
+ if uri
223
+ config['database']['mongodb'] ||= {}
224
+ config['database']['mongodb']['uri'] = uri
225
+ elsif !ENV['EKSA_MONGODB_URI'] && !ENV['MONGODB_URI'] && !config.dig('database', 'mongodb', 'uri')
226
+ puts "⚠️ Peringatan: Anda memilih MongoDB tapi belum memberikan URI."
227
+ puts " Gunakan: export EKSA_MONGODB_URI=\"YOUR_URI\""
228
+ puts " Atau: eksa db switch mongo \"YOUR_URI\""
229
+ end
230
+ end
231
+
232
+ File.write(config_path, JSON.pretty_generate(config))
233
+ puts "⚙️ Database berhasil diubah ke: #{config['database']['type'].upcase}."
234
+ end
235
+
236
+ def migrate_database(from_type, to_type)
237
+ require_relative '../lib/eksa/database/sqlite_adapter'
238
+ require_relative '../lib/eksa/database/mongo_adapter'
239
+ require 'json'
240
+
241
+ config_path = File.join(Dir.pwd, '.eksa.json')
242
+ config = File.exist?(config_path) ? JSON.parse(File.read(config_path)) : {}
243
+
244
+ puts "🚀 Memulai migrasi data dari #{from_type.upcase} ke #{to_type.upcase}..."
245
+
246
+ source_adapter = create_specific_adapter(from_type, config)
247
+ target_adapter = create_specific_adapter(to_type, config)
248
+
249
+ # Migration logic for User model (the only one currently using Eksa::Model)
250
+ puts " 📦 Memindahkan tabel 'eksa_users'..."
251
+
252
+ begin
253
+ # Source data
254
+ users = source_adapter.execute("SELECT id, username, password_hash FROM eksa_users")
255
+
256
+ # Target setup
257
+ target_adapter.execute("CREATE TABLE IF NOT EXISTS eksa_users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)")
258
+
259
+ count = 0
260
+ users.each do |user|
261
+ # user is [id, username, password_hash]
262
+ # Check if user already exists in target
263
+ existing = target_adapter.execute("SELECT id FROM eksa_users WHERE username = ?", [user[1]])
264
+
265
+ if existing.empty?
266
+ target_adapter.execute("INSERT INTO eksa_users (username, password_hash) VALUES (?, ?)", [user[1], user[2]])
267
+ count += 1
268
+ else
269
+ puts " ⚠️ User '#{user[1]}' sudah ada di target, melewati..."
270
+ end
271
+ end
272
+
273
+ puts " ✅ Berhasil memindahkan #{count} user."
274
+ puts "\n🎉 Migrasi Selesai!"
275
+ rescue => e
276
+ puts "❌ Terjadi kesalahan saat migrasi: #{e.message}"
277
+ end
278
+ end
279
+
280
+ def create_specific_adapter(type, config)
281
+ case type.downcase
282
+ when 'mongo', 'mongodb'
283
+ require_relative '../lib/eksa/database/mongo_adapter'
284
+ mongo_config = config.dig('database', 'mongodb') || {}
285
+
286
+ # Load .env for CLI
287
+ begin
288
+ require 'dotenv'
289
+ Dotenv.load if File.exist?('.env')
290
+ rescue LoadError
291
+ end
292
+
293
+ env_uri = ENV['EKSA_MONGODB_URI'] || ENV['MONGODB_URI']
294
+ mongo_config['uri'] = env_uri if env_uri
295
+
296
+ Eksa::MongoAdapter.new(mongo_config)
297
+ else
298
+ require_relative '../lib/eksa/database/sqlite_adapter'
299
+ Eksa::SqliteAdapter.new(config.dig('database', 'sqlite'))
300
+ end
301
+ end
302
+
145
303
  def start_server
146
304
  if File.exist?('config.ru')
147
305
  puts "🚀 Memulai Eksa Framework Server..."
@@ -179,6 +337,51 @@ when 'g', 'generate'
179
337
  else
180
338
  usage
181
339
  end
340
+ when 'feature'
341
+ action = ARGV.shift
342
+ feature = ARGV.shift
343
+ if action && feature
344
+ require 'json'
345
+ toggle_feature(action, feature)
346
+ else
347
+ usage
348
+ end
349
+ when 'reset-password'
350
+ username = ARGV.shift
351
+ password = ARGV.shift
352
+ if username && password
353
+ reset_password(username, password)
354
+ else
355
+ puts "❌ Error: Argumen tidak lengkap. Gunakan: eksa reset-password USERNAME NEW_PASSWORD"
356
+ end
357
+ when 'db'
358
+ subcommand = ARGV.shift
359
+ case subcommand
360
+ when 'switch'
361
+ type = ARGV.shift
362
+ uri = ARGV.shift
363
+ if type
364
+ switch_database(type, uri)
365
+ else
366
+ puts "❌ Error: Tipe database harus diisi. Gunakan: eksa db switch [sqlite|mongo]"
367
+ end
368
+ when 'migrate'
369
+ from = nil
370
+ to = nil
371
+ while (arg = ARGV.shift)
372
+ case arg
373
+ when '--from' then from = ARGV.shift
374
+ when '--to' then to = ARGV.shift
375
+ end
376
+ end
377
+ if from && to
378
+ migrate_database(from, to)
379
+ else
380
+ puts "❌ Error: Argumen tidak lengkap. Gunakan: eksa db migrate --from TYPE --to TYPE"
381
+ end
382
+ else
383
+ usage
384
+ end
182
385
  else
183
386
  usage
184
387
  end
@@ -0,0 +1,50 @@
1
+ module Eksa
2
+ class AuthController < Eksa::Controller
3
+ def login
4
+ return redirect_to "/cms" if current_user
5
+ render_internal 'auth/login'
6
+ end
7
+
8
+ def register
9
+ return redirect_to "/cms" if current_user
10
+ @admin_exists = Eksa::User.all.any?
11
+ render_internal 'auth/register'
12
+ end
13
+
14
+ def process_login
15
+ username = params['username']
16
+ password = params['password']
17
+
18
+ user = Eksa::User.authenticate(username, password)
19
+ if user
20
+ session['user_id'] = user[:id]
21
+ redirect_to "/cms", notice: "Selamat datang kembali, #{username}!"
22
+ else
23
+ redirect_to "/auth/login", notice: "Username atau password salah."
24
+ end
25
+ end
26
+
27
+ def process_register
28
+ if Eksa::User.all.any?
29
+ return redirect_to "/auth/login", notice: "Registrasi ditutup. Hanya satu admin yang diizinkan."
30
+ end
31
+
32
+ username = params['username']
33
+ password = params['password']
34
+
35
+ if username && !username.empty? && password && password.length >= 6
36
+ Eksa::User.create(username, password)
37
+ user = Eksa::User.authenticate(username, password)
38
+ session['user_id'] = user[:id]
39
+ redirect_to "/cms", notice: "Akun berhasil dibuat. Selamat datang!"
40
+ else
41
+ redirect_to "/auth/register", notice: "Data tidak valid. Password minimal 6 karakter."
42
+ end
43
+ end
44
+
45
+ def logout
46
+ session.delete('user_id')
47
+ redirect_to "/", notice: "Anda telah berhasil logout."
48
+ end
49
+ end
50
+ end