fix data setting get datatable using api token

This commit is contained in:
arifal
2025-06-17 11:54:02 +07:00
parent a8b02afad9
commit 285ff46c2b
10 changed files with 586 additions and 56 deletions

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ClearDatabaseSessions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'session:clear-db {--force : Force the operation without confirmation}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear all database sessions';
/**
* Execute the console command.
*/
public function handle()
{
if (!$this->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;
}
}
}

View File

@@ -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.
*

View File

@@ -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,
];
}
}

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
app:
build:

View File

@@ -11,15 +11,80 @@ class DataSettings {
this.toast = new bootstrap.Toast(this.toastElement);
this.table = null;
// Initialize functions
// Initialize immediately
this.init();
}
/**
* 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!";
const result = await response.json();
this.toastMessage.innerText = result.message || "Deleted successfully!";
this.toast.show();
// Refresh Grid.js table
if (typeof this.table !== "undefined") {
if (this.table) {
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();
}
} 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();
});

View File

@@ -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<string|null>}
*/
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<boolean>}
*/
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<Response>}
*/
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;

View File

@@ -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<any>}
*/
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<any>}
*/
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<any>}
*/
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<any>}
*/
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<any>}
*/
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<any>}
*/
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;

View File

@@ -14,11 +14,18 @@
<div class="col-12">
<div class="card w-100">
<div class="card-body">
<div class="d-flex flex-wrap justify-content-end align-items-center mb-2">
<div class="d-flex flex-wrap justify-content-between align-items-center mb-2">
<div class="d-flex gap-2">
<!-- Space for future buttons if needed -->
</div>
<div>
@if ($creator)
<a href="{{ route('data-settings.create', ['menu_id' => $menuId])}}" class="btn btn-success btn-sm d-block d-sm-inline w-auto">Create</a>
@endif
</div>
</div>
<div id="table-data-settings"
data-updater="{{ $updater }}"
data-destroyer="{{ $destroyer }}"

View File

@@ -8,8 +8,21 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="robots" content="index, follow" />
<meta name="theme-color" content="#ffffff">
<!-- Authentication Tokens -->
<meta name="csrf-token" content="{{ csrf_token() }}">
@auth
<!-- API Token from session (primary method) -->
<meta name="api-token" content="{{ session('api_token') }}">
<!-- Alternative: User ID for token generation -->
<meta name="user-id" content="{{ auth()->id() }}">
<!-- Token expiration timestamp (if available) -->
@if(session('api_token_expires'))
<meta name="api-token-expires" content="{{ session('api_token_expires') }}">
@endif
@endauth
<!-- App favicon -->
<link rel="shortcut icon" href="/images/dputr.ico">

View File

@@ -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');