diff --git a/app/Console/Commands/ClearDatabaseSessions.php b/app/Console/Commands/ClearDatabaseSessions.php new file mode 100644 index 0000000..6751a8f --- /dev/null +++ b/app/Console/Commands/ClearDatabaseSessions.php @@ -0,0 +1,45 @@ +option('force') && !$this->confirm('Are you sure you want to clear all database sessions?')) { + $this->info('Operation cancelled.'); + return 0; + } + + try { + $count = DB::table('sessions')->count(); + DB::table('sessions')->delete(); + + $this->info("Successfully cleared {$count} database sessions."); + return 0; + } catch (\Exception $e) { + $this->error('Failed to clear database sessions: ' . $e->getMessage()); + return 1; + } + } +} diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index 4164ee6..748ee64 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -36,13 +36,68 @@ class AuthenticatedSessionController extends Controller // Ambil user yang sedang login $user = Auth::user(); - // Buat token untuk API - $token = $user->createToken(env('APP_KEY'))->plainTextToken; + // Hapus token lama jika ada + $user->tokens()->delete(); + + // Buat token untuk API dengan scope dan expiration + $tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time(); + + // Token dengan scope (opsional) + $token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken; + + // Simpan token di session untuk digunakan di frontend session(['api_token' => $token]); return redirect()->intended(RouteServiceProvider::HOME); } + /** + * Generate API token for authenticated user + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function generateApiToken(Request $request) + { + $user = Auth::user(); + + if (!$user) { + return response()->json(['error' => 'Unauthorized'], 401); + } + + // Delete existing tokens + $user->tokens()->delete(); + + // Generate new token + $tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time(); + $token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken; + + return response()->json([ + 'token' => $token, + 'token_type' => 'Bearer', + 'expires_in' => 30 * 24 * 60 * 60, // 30 days in seconds + ]); + } + + /** + * Revoke API token for authenticated user + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function revokeApiToken(Request $request) + { + $user = Auth::user(); + + if (!$user) { + return response()->json(['error' => 'Unauthorized'], 401); + } + + $user->tokens()->delete(); + + return response()->json(['message' => 'All tokens revoked successfully']); + } + /** * Destroy an authenticated session. * diff --git a/app/Http/Resources/DataSettingResource.php b/app/Http/Resources/DataSettingResource.php index 3cc1d14..a9bc4d7 100644 --- a/app/Http/Resources/DataSettingResource.php +++ b/app/Http/Resources/DataSettingResource.php @@ -19,8 +19,8 @@ class DataSettingResource extends JsonResource 'key' => $this->key, 'value' => $this->value, 'type' => $this->type, - 'created_at' => $this->created_at->toDateTimeString(), - 'updated_at' => $this->updated_at->toDateTimeString(), + 'created_at' => $this->created_at ? $this->created_at->toDateTimeString() : null, + 'updated_at' => $this->updated_at ? $this->updated_at->toDateTimeString() : null, ]; } } diff --git a/docker-compose.yml b/docker-compose.yml index 44fb872..45e07e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: app: build: diff --git a/resources/js/data-settings/index.js b/resources/js/data-settings/index.js index 5c9e6e1..3d9aef3 100644 --- a/resources/js/data-settings/index.js +++ b/resources/js/data-settings/index.js @@ -10,16 +10,81 @@ class DataSettings { this.toastElement = document.getElementById("toastNotification"); this.toast = new bootstrap.Toast(this.toastElement); this.table = null; + + // Initialize immediately + this.init(); + } - // Initialize functions + /** + * Initialize the DataSettings class + */ + init() { this.initTableDataSettings(); this.initEvents(); } + + /** + * Get API token from meta tag + * @returns {string|null} + */ + getApiToken() { + const tokenMeta = document.querySelector('meta[name="api-token"]'); + return tokenMeta ? tokenMeta.getAttribute('content') : null; + } + + /** + * Get authentication headers for API requests + * @returns {object} + */ + getAuthHeaders() { + const token = this.getApiToken(); + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (csrfToken) { + headers['X-CSRF-TOKEN'] = csrfToken; + } + + return headers; + } + + /** + * Make API request with authentication + * @param {string} url + * @param {object} options + * @returns {Promise} + */ + async makeApiRequest(url, options = {}) { + const defaultOptions = { + headers: this.getAuthHeaders(), + ...options + }; + + try { + const response = await fetch(url, defaultOptions); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response; + } catch (error) { + console.error('API Request failed:', error); + throw error; + } + } + initEvents() { document.body.addEventListener("click", async (event) => { - const deleteButton = event.target.closest( - ".btn-delete-data-settings" - ); + const deleteButton = event.target.closest(".btn-delete-data-settings"); if (deleteButton) { event.preventDefault(); await this.handleDelete(deleteButton); @@ -34,7 +99,8 @@ class DataSettings { let canUpdate = tableContainer.getAttribute("data-updater") === "1"; let canDelete = tableContainer.getAttribute("data-destroyer") === "1"; let menuId = tableContainer.getAttribute("data-menuId"); - // Create a new Grid.js instance only if it doesn't exist + + // Create a new Grid.js instance this.table = new Grid({ columns: [ "ID", @@ -77,9 +143,7 @@ class DataSettings { limit: 15, server: { url: (prev, page) => - `${prev}${prev.includes("?") ? "&" : "?"}page=${ - page + 1 - }`, + `${prev}${prev.includes("?") ? "&" : "?"}page=${page + 1}`, }, }, sort: true, @@ -91,13 +155,7 @@ class DataSettings { }, server: { url: `${GlobalConfig.apiHost}/api/data-settings`, - headers: { - "X-CSRF-TOKEN": document - .querySelector('meta[name="csrf-token"]') - .getAttribute("content"), - "Content-Type": "application/json", - "Accept": "application/json", - }, + headers: this.getAuthHeaders(), then: (data) => data.data.map((item) => [ item.id, @@ -110,6 +168,7 @@ class DataSettings { }, }).render(tableContainer); } + async handleDelete(deleteButton) { const id = deleteButton.getAttribute("data-id"); @@ -125,46 +184,29 @@ class DataSettings { if (result.isConfirmed) { try { - let response = await fetch( + const response = await this.makeApiRequest( `${GlobalConfig.apiHost}/api/data-settings/${id}`, - { - method: "DELETE", - credentials: "include", - headers: { - "X-CSRF-TOKEN": document - .querySelector('meta[name="csrf-token"]') - .getAttribute("content"), - "Content-Type": "application/json", - "Accept": "application/json", - }, - } + { method: "DELETE" } ); - if (response.ok) { - let result = await response.json(); - this.toastMessage.innerText = - result.message || "Deleted successfully!"; - this.toast.show(); + const result = await response.json(); + this.toastMessage.innerText = result.message || "Deleted successfully!"; + this.toast.show(); - // Refresh Grid.js table - if (typeof this.table !== "undefined") { - this.table.updateConfig({}).forceRender(); - } - } else { - let error = await response.json(); - console.error("Delete failed:", error); - this.toastMessage.innerText = - error.message || "Delete failed!"; - this.toast.show(); + // Refresh Grid.js table + if (this.table) { + this.table.updateConfig({}).forceRender(); } } catch (error) { console.error("Error deleting item:", error); - this.toastMessage.innerText = "An error occurred!"; + this.toastMessage.innerText = error.message || "An error occurred!"; this.toast.show(); } } } } -document.addEventListener("DOMContentLoaded", function (e) { + +// Initialize when DOM is ready +document.addEventListener("DOMContentLoaded", function () { new DataSettings(); }); diff --git a/resources/js/utils/api-token-manager.js b/resources/js/utils/api-token-manager.js new file mode 100644 index 0000000..c5cb0fb --- /dev/null +++ b/resources/js/utils/api-token-manager.js @@ -0,0 +1,198 @@ +/** + * API Token Manager Utility + * Handles API token generation, storage, and usage + */ +class ApiTokenManager { + constructor() { + this.token = null; + this.tokenKey = 'api_token'; + this.init(); + } + + /** + * Initialize token manager + */ + init() { + // Try to get token from meta tag first (session-based) + const metaToken = document.querySelector('meta[name="api-token"]'); + if (metaToken && metaToken.getAttribute('content')) { + this.token = metaToken.getAttribute('content'); + } else { + // Try to get from localStorage as fallback + this.token = localStorage.getItem(this.tokenKey); + } + } + + /** + * Generate new API token + * @returns {Promise} + */ + async generateToken() { + try { + const response = await fetch('/api-tokens/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + credentials: 'include' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (data.token) { + this.setToken(data.token); + return data.token; + } + + throw new Error('No token received'); + } catch (error) { + console.error('Failed to generate token:', error); + return null; + } + } + + /** + * Revoke current API token + * @returns {Promise} + */ + async revokeToken() { + try { + const response = await fetch('/api-tokens/revoke', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'), + 'Authorization': `Bearer ${this.token}` + }, + credentials: 'include' + }); + + if (response.ok) { + this.clearToken(); + return true; + } + + return false; + } catch (error) { + console.error('Failed to revoke token:', error); + return false; + } + } + + /** + * Set token and update storage + * @param {string} token + */ + setToken(token) { + this.token = token; + localStorage.setItem(this.tokenKey, token); + + // Update meta tag if exists + const metaTag = document.querySelector('meta[name="api-token"]'); + if (metaTag) { + metaTag.setAttribute('content', token); + } + } + + /** + * Get current token + * @returns {string|null} + */ + getToken() { + return this.token; + } + + /** + * Clear token from storage + */ + clearToken() { + this.token = null; + localStorage.removeItem(this.tokenKey); + + // Clear meta tag if exists + const metaTag = document.querySelector('meta[name="api-token"]'); + if (metaTag) { + metaTag.setAttribute('content', ''); + } + } + + /** + * Check if token exists + * @returns {boolean} + */ + hasToken() { + return this.token !== null && this.token !== ''; + } + + /** + * Get authorization header + * @returns {object} + */ + getAuthHeader() { + if (!this.hasToken()) { + return {}; + } + + return { + 'Authorization': `Bearer ${this.token}` + }; + } + + /** + * Make authenticated API request + * @param {string} url + * @param {object} options + * @returns {Promise} + */ + async request(url, options = {}) { + const defaultOptions = { + headers: { + 'Content-Type': 'application/json', + ...this.getAuthHeader(), + ...(options.headers || {}) + }, + credentials: 'include' + }; + + const mergedOptions = { + ...defaultOptions, + ...options, + headers: { + ...defaultOptions.headers, + ...(options.headers || {}) + } + }; + + try { + const response = await fetch(url, mergedOptions); + + // If unauthorized, try to generate new token + if (response.status === 401 && this.hasToken()) { + console.log('Token expired, generating new token...'); + const newToken = await this.generateToken(); + + if (newToken) { + // Retry request with new token + mergedOptions.headers.Authorization = `Bearer ${newToken}`; + return fetch(url, mergedOptions); + } + } + + return response; + } catch (error) { + console.error('API request failed:', error); + throw error; + } + } +} + +// Export singleton instance +const apiTokenManager = new ApiTokenManager(); +window.ApiTokenManager = apiTokenManager; + +export default apiTokenManager; \ No newline at end of file diff --git a/resources/js/utils/modern-api-helper.js b/resources/js/utils/modern-api-helper.js new file mode 100644 index 0000000..9796da6 --- /dev/null +++ b/resources/js/utils/modern-api-helper.js @@ -0,0 +1,166 @@ +/** + * Modern API Helper with Token Management + */ +import ApiTokenManager from './api-token-manager.js'; + +class ApiHelper { + constructor() { + this.baseUrl = window.GlobalConfig?.apiHost || ''; + this.tokenManager = ApiTokenManager; + } + + /** + * Make GET request + * @param {string} endpoint + * @param {object} options + * @returns {Promise} + */ + async get(endpoint, options = {}) { + return this.request(endpoint, { + method: 'GET', + ...options + }); + } + + /** + * Make POST request + * @param {string} endpoint + * @param {object} data + * @param {object} options + * @returns {Promise} + */ + async post(endpoint, data = null, options = {}) { + return this.request(endpoint, { + method: 'POST', + body: data ? JSON.stringify(data) : null, + ...options + }); + } + + /** + * Make PUT request + * @param {string} endpoint + * @param {object} data + * @param {object} options + * @returns {Promise} + */ + async put(endpoint, data = null, options = {}) { + return this.request(endpoint, { + method: 'PUT', + body: data ? JSON.stringify(data) : null, + ...options + }); + } + + /** + * Make DELETE request + * @param {string} endpoint + * @param {object} options + * @returns {Promise} + */ + async delete(endpoint, options = {}) { + return this.request(endpoint, { + method: 'DELETE', + ...options + }); + } + + /** + * Make authenticated request using token manager + * @param {string} endpoint + * @param {object} options + * @returns {Promise} + */ + async request(endpoint, options = {}) { + const url = `${this.baseUrl}${endpoint}`; + + try { + const response = await this.tokenManager.request(url, options); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('API request failed:', error); + throw error; + } + } + + /** + * Upload file with authentication + * @param {string} endpoint + * @param {FormData} formData + * @param {object} options + * @returns {Promise} + */ + async upload(endpoint, formData, options = {}) { + const url = `${this.baseUrl}${endpoint}`; + + const uploadOptions = { + method: 'POST', + body: formData, + headers: { + // Don't set Content-Type for FormData, let browser set it + ...this.tokenManager.getAuthHeader(), + ...(options.headers || {}) + }, + credentials: 'include', + ...options + }; + + try { + const response = await fetch(url, uploadOptions); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('Upload failed:', error); + throw error; + } + } + + /** + * Download file with authentication + * @param {string} endpoint + * @param {string} filename + * @param {object} options + */ + async download(endpoint, filename = null, options = {}) { + const url = `${this.baseUrl}${endpoint}`; + + try { + const response = await this.tokenManager.request(url, { + method: 'GET', + ...options + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = filename || 'download'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(downloadUrl); + } catch (error) { + console.error('Download failed:', error); + throw error; + } + } +} + +// Export singleton instance +const apiHelper = new ApiHelper(); +window.ApiHelper = apiHelper; + +export default apiHelper; \ No newline at end of file diff --git a/resources/views/data-settings/index.blade.php b/resources/views/data-settings/index.blade.php index 6192749..0f197e5 100644 --- a/resources/views/data-settings/index.blade.php +++ b/resources/views/data-settings/index.blade.php @@ -14,15 +14,22 @@
-
- @if ($creator) - Create - @endif +
+
+ +
+ +
+ @if ($creator) + Create + @endif +
+
+ data-menuId="{{ $menuId }}">
diff --git a/resources/views/layouts/partials/title-meta.blade.php b/resources/views/layouts/partials/title-meta.blade.php index 61a2180..69d0c28 100644 --- a/resources/views/layouts/partials/title-meta.blade.php +++ b/resources/views/layouts/partials/title-meta.blade.php @@ -8,8 +8,21 @@ + + - +@auth + + + + + + + + @if(session('api_token_expires')) + + @endif +@endauth \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 8447b4c..fd59f47 100644 --- a/routes/web.php +++ b/routes/web.php @@ -48,6 +48,12 @@ Route::group(['middleware' => 'auth'], function(){ Route::get('', [BigDataController::class, 'index'])->name('any'); Route::get('/home', [HomeController::class, 'index'])->name('home'); + // API Token Management + Route::prefix('api-tokens')->group(function() { + Route::post('/generate', [\App\Http\Controllers\Auth\AuthenticatedSessionController::class, 'generateApiToken'])->name('api-tokens.generate'); + Route::delete('/revoke', [\App\Http\Controllers\Auth\AuthenticatedSessionController::class, 'revokeApiToken'])->name('api-tokens.revoke'); + }); + //dashboards Route::group(['prefix' => '/dashboards'], function(){ Route::get('/bigdata', [BigDataController::class, 'index'])->name('dashboard.home');