Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions app/Filament/Resources/TokenResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

namespace App\Filament\Resources;

use Filament\Forms;
use Filament\Tables;
use App\Models\Token;
use Filament\Resources\Form;
use Filament\Resources\Table;
use Illuminate\Support\Carbon;
use Filament\Resources\Resource;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use App\Filament\Resources\TokenResource\Pages;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use App\Filament\Resources\TokenResource\RelationManagers;

class TokenResource extends Resource
{
protected static ?string $model = Token::class;

protected static ?string $navigationIcon = 'heroicon-o-key';

// tambahkan ini supaya muncul di MANAGEMENT
protected static ?int $navigationSort = 5;
protected static ?string $navigationGroup = 'Management';
protected static ?string $navigationLabel = 'Tokens';

public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Card::make()
->schema([
Forms\Components\Grid::make()
->schema([
Forms\Components\TextInput::make('name')
->label('Token Name')
->required(),

Forms\Components\Select::make('tokenable_id')
->label('User')
->searchable()
->options(fn() => \App\Models\User::pluck('name', 'id'))
->required(),

// penting: tambahkan ini biar tokenable_type otomatis ke User
Forms\Components\Hidden::make('tokenable_type')
->default(\App\Models\User::class)
->required(),

Forms\Components\DateTimePicker::make('expires_at')
->label('Expired At')
->default(now('Asia/Jakarta')->addYear())
->required(),
])
])
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')->sortable(),

Tables\Columns\TextColumn::make('plain_token')
->label('Token')
->copyable()
->toggleable(isToggledHiddenByDefault: false),

Tables\Columns\TextColumn::make('tokenable.name')
->label('User')
->sortable()
->searchable(),

Tables\Columns\TextColumn::make('expires_at')
->label('Expired At')
->dateTime()
->sortable()
->formatStateUsing(function ($record) {
if ($record->expires_at && $record->expires_at->isPast()) {
return 'Token expired';
}
return $record->expires_at ? $record->expires_at->format('M d, Y H:i:s') : '-';
})
->color(function ($record) {
return $record->expires_at && $record->expires_at->isPast() ? 'danger' : null;
}),
Tables\Columns\TextColumn::make('last_used_at')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->label('Created At'),
])
->filters([])
->actions([
Tables\Actions\Action::make('copy_token')
->label('Copy Token')
->icon('heroicon-o-clipboard')
->color('success') // hijau
->action(function (Token $record, $livewire) {
$livewire->dispatchBrowserEvent('copy-token', [
'token' => $record->plain_token,
]);

Notification::make()
->success()
->title('Token copied!')
->send();
}),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
]);
}

public static function getRelations(): array
{
return [
//
];
}

public static function getPages(): array
{
return [
'index' => Pages\ListTokens::route('/'),
'create' => Pages\CreateToken::route('/create'),
'edit' => Pages\EditToken::route('/{record}/edit'),
];
}
}
42 changes: 42 additions & 0 deletions app/Filament/Resources/TokenResource/Pages/CreateToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace App\Filament\Resources\TokenResource\Pages;

use App\Models\User;
use Filament\Pages\Actions;
use Illuminate\Support\Carbon;
use App\Filament\Resources\TokenResource;
use Filament\Resources\Pages\CreateRecord;

class CreateToken extends CreateRecord
{
protected static string $resource = TokenResource::class;

protected function handleRecordCreation(array $data): \Illuminate\Database\Eloquent\Model
{
// Ambil user dari tokenable_id
$user = User::findOrFail($data['tokenable_id']);

// Buat token via Sanctum (ini yang generate otomatis field "token")
$token = $user->createToken(
$data['name'], // nama token
['*'], // abilities
Carbon::parse($data['expires_at']) // expired
);

// simpan plain token di DB
$accessToken = $token->accessToken;
$accessToken->plain_token = $token->plainTextToken;
$accessToken->save();

session()->flash('generated_token', $token->plainTextToken);

return $token->accessToken;
}

protected function getRedirectUrl(): string
{
// setelah simpan, redirect ke list
return $this->getResource()::getUrl('index');
}
}
42 changes: 42 additions & 0 deletions app/Filament/Resources/TokenResource/Pages/EditToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace App\Filament\Resources\TokenResource\Pages;

use App\Filament\Resources\TokenResource;
use Filament\Pages\Actions;
use Filament\Resources\Pages\EditRecord;

class EditToken extends EditRecord
{
protected static string $resource = TokenResource::class;

protected function getActions(): array
{
return [
Actions\DeleteAction::make(),
];
}

/**
* Override update agar token tidak digenerate ulang,
* cukup update expires_at atau name saja.
*/
protected function handleRecordUpdate(\Illuminate\Database\Eloquent\Model $record, array $data): \Illuminate\Database\Eloquent\Model
{
// update hanya field non-token
$record->update([
'name' => $data['name'],
'expires_at' => $data['expires_at'],
'tokenable_id' => $data['tokenable_id'],
'tokenable_type' => $data['tokenable_type'],
]);

return $record;
}

protected function getRedirectUrl(): string
{
// setelah simpan, redirect ke list
return $this->getResource()::getUrl('index');
}
}
25 changes: 25 additions & 0 deletions app/Filament/Resources/TokenResource/Pages/ListTokens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Filament\Resources\TokenResource\Pages;

use App\Filament\Resources\TokenResource;
use Filament\Pages\Actions;
use Filament\Resources\Pages\ListRecords;

class ListTokens extends ListRecords
{
protected static string $resource = TokenResource::class;

protected function getFooter(): ?\Illuminate\Contracts\View\View
{
return view('filament.resources.token-resource.copy-token');
}

protected function getActions(): array
{
return [
Actions\CreateAction::make(),
];
}

}
3 changes: 2 additions & 1 deletion app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class Kernel extends HttpKernel
],

'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\App\Http\Middleware\CheckTokenExpiry::class, // middleware untuk cek token expired
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
Expand Down
30 changes: 30 additions & 0 deletions app/Http/Middleware/CheckTokenExpiry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Laravel\Sanctum\PersonalAccessToken;
use Symfony\Component\HttpFoundation\Response;

class CheckTokenExpiry
{
public function handle(Request $request, Closure $next)
{
$token = $request->bearerToken();

if ($token) {
// Cari token berdasarkan plain_token
$accessToken = \App\Models\Token::where('plain_token', $token)->first();

// Jika token ditemukan tetapi kadaluarsa
if ($accessToken && $accessToken->expires_at && $accessToken->expires_at->isPast()) {
return response()->json([
'message' => 'Token expired'
], Response::HTTP_UNAUTHORIZED);
}
}

return $next($request);
}
}
43 changes: 43 additions & 0 deletions app/Models/Token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Token extends Model
{
protected $table = 'personal_access_tokens';

protected $fillable = [
'name',
'token',
'abilities',
'last_used_at',
'expires_at',
'plain_token',
'tokenable_id',
'tokenable_type',
];

protected $hidden = [
'token',
];

protected $casts = [
'last_used_at' => 'datetime',
'expires_at' => 'datetime',
];

/**
* Relasi ke User (atau model lain yg bisa punya token).
*/
public function user()
{
return $this->morphTo('tokenable');
}

public function tokenable()
{
return $this->morphTo();
}
}
3 changes: 2 additions & 1 deletion catatan_rilis.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Di rilis v2507.0.0 berisi penambahan fitur dan perbaikan lain sesuai dengan pela
6. [#12](https://github.com/OpenSID/help-desk/issues/12) Fitur : Master Milestone
7. [#22](https://github.com/OpenSID/help-desk/issues/22) Fitur: Menambahkan username github pada data user
8. [#12](https://github.com/OpenSID/help-desk/issues/21) Fitur: Sinkron tiket ke github service issue dan project
98. [#13](https://github.com/OpenSID/help-desk/issues/13) Fitur: Laporan Tiket Dalam grafik
9. [#13](https://github.com/OpenSID/help-desk/issues/13) Fitur: Laporan Tiket Dalam grafik
10. [#96](https://github.com/OpenSID/DukunganTeknis/issues/96) Fitur: Modul token untuk API

#### Perbaikan BUG

Expand Down
2 changes: 1 addition & 1 deletion config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
|
*/

'timezone' => 'UTC',
'timezone' => 'Asia/Jakarta',

/*
|--------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('personal_access_tokens', function (Blueprint $table) {
$table->text('plain_token')->nullable()->after('token');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('personal_access_tokens', function (Blueprint $table) {
$table->dropColumn('plain_token');
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
window.addEventListener('copy-token', event => {
navigator.clipboard.writeText(event.detail.token);
});
</script>
7 changes: 7 additions & 0 deletions resources/views/livewire/copy-token-action.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div>
<x-filament::button size="sm" color="success" icon="heroicon-o-clipboard">
Copy Token
</x-filament::button>


</div>
Loading