stage 2 of oauth removal, switch to sanctum pat tokens completed, docs updated

This commit is contained in:
Codex Agent
2025-11-07 07:46:53 +01:00
parent 776da57ca9
commit 67affd3317
41 changed files with 124 additions and 2148 deletions

View File

@@ -1,190 +0,0 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\OAuthClientResource\Pages;
use App\Models\OAuthClient;
use App\Models\RefreshToken;
use BackedEnum;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class OAuthClientResource extends Resource
{
protected static ?string $model = OAuthClient::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-key';
protected static ?int $navigationSort = 30;
public static function getNavigationGroup(): string
{
return __('admin.nav.security');
}
public static function form(Schema $form): Schema
{
return $form->schema([
Forms\Components\TextInput::make('name')
->label(__('admin.oauth.fields.name'))
->required()
->maxLength(255),
Forms\Components\TextInput::make('client_id')
->label(__('admin.oauth.fields.client_id'))
->required()
->unique(ignoreRecord: true)
->maxLength(255),
Forms\Components\TextInput::make('client_secret')
->label(__('admin.oauth.fields.client_secret'))
->password()
->revealable()
->helperText(__('admin.oauth.hints.client_secret'))
->dehydrated(fn (?string $state): bool => filled($state))
->dehydrateStateUsing(fn (?string $state): ?string => filled($state) ? Hash::make($state) : null),
Forms\Components\Select::make('tenant_id')
->label(__('admin.oauth.fields.tenant'))
->relationship('tenant', 'name')
->searchable()
->preload()
->nullable(),
Forms\Components\Textarea::make('redirect_uris')
->label(__('admin.oauth.fields.redirect_uris'))
->rows(4)
->helperText(__('admin.oauth.hints.redirect_uris'))
->formatStateUsing(fn ($state): string => is_array($state) ? implode(PHP_EOL, $state) : (string) $state)
->dehydrateStateUsing(function (?string $state): array {
$entries = collect(preg_split('/\r\n|\r|\n/', (string) $state))
->map(fn ($uri) => trim($uri))
->filter();
return $entries->values()->all();
})
->required(),
Forms\Components\TagsInput::make('scopes')
->label(__('admin.oauth.fields.scopes'))
->placeholder('tenant:read')
->suggestions([
'tenant:read',
'tenant:write',
'tenant:admin',
])
->separator(',')
->required(),
Forms\Components\Toggle::make('is_active')
->label(__('admin.oauth.fields.is_active'))
->default(true),
Forms\Components\Textarea::make('description')
->label(__('admin.oauth.fields.description'))
->rows(3)
->columnSpanFull(),
])->columns(2);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label(__('admin.oauth.fields.name'))
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('client_id')
->label(__('admin.oauth.fields.client_id'))
->copyable()
->sortable(),
Tables\Columns\TextColumn::make('tenant.name')
->label(__('admin.oauth.fields.tenant'))
->toggleable(isToggledHiddenByDefault: true)
->sortable(),
Tables\Columns\IconColumn::make('is_active')
->label(__('admin.oauth.fields.is_active'))
->boolean()
->color(fn (bool $state): string => $state ? 'success' : 'danger'),
Tables\Columns\TextColumn::make('redirect_uris')
->label(__('admin.oauth.fields.redirect_uris'))
->formatStateUsing(fn ($state) => collect(Arr::wrap($state))->implode("\n"))
->limit(50)
->toggleable(),
Tables\Columns\TagsColumn::make('scopes')
->label(__('admin.oauth.fields.scopes'))
->separator(', ')
->limit(4),
Tables\Columns\TextColumn::make('updated_at')
->label(__('admin.oauth.fields.updated_at'))
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\TernaryFilter::make('is_active')
->label(__('admin.oauth.filters.is_active'))
->placeholder(__('admin.oauth.filters.any'))
->trueLabel(__('admin.oauth.filters.active'))
->falseLabel(__('admin.oauth.filters.inactive')),
Tables\Filters\SelectFilter::make('tenant_id')
->label(__('admin.oauth.fields.tenant'))
->relationship('tenant', 'name')
->searchable(),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
Tables\Actions\Action::make('regenerate_secret')
->label(__('admin.oauth.actions.regenerate_secret'))
->icon('heroicon-o-arrow-path')
->requiresConfirmation()
->action(function (OAuthClient $record): void {
$plainSecret = Str::random(48);
$record->forceFill([
'client_secret' => Hash::make($plainSecret),
])->save();
Notification::make()
->title(__('admin.oauth.notifications.secret_regenerated_title'))
->body(__('admin.oauth.notifications.secret_regenerated_body', [
'secret' => $plainSecret,
]))
->success()
->send();
}),
Tables\Actions\DeleteAction::make()
->before(function (OAuthClient $record): void {
RefreshToken::query()
->where('client_id', $record->client_id)
->delete();
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make()
->before(function (Collection $records): void {
$records->each(function (OAuthClient $record) {
RefreshToken::query()
->where('client_id', $record->client_id)
->delete();
});
}),
]),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListOAuthClients::route('/'),
'create' => Pages\CreateOAuthClient::route('/create'),
'view' => Pages\ViewOAuthClient::route('/{record}'),
'edit' => Pages\EditOAuthClient::route('/{record}/edit'),
];
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Filament\Resources\OAuthClientResource\Pages;
use App\Filament\Resources\OAuthClientResource;
use Filament\Resources\Pages\CreateRecord;
class CreateOAuthClient extends CreateRecord
{
protected static string $resource = OAuthClientResource::class;
protected function getCreatedNotificationTitle(): ?string
{
return __('admin.oauth.notifications.created_title');
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Filament\Resources\OAuthClientResource\Pages;
use App\Filament\Resources\OAuthClientResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditOAuthClient extends EditRecord
{
protected static string $resource = OAuthClientResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
protected function getSavedNotificationTitle(): ?string
{
return __('admin.oauth.notifications.updated_title');
}
}

View File

@@ -1,12 +0,0 @@
<?php
namespace App\Filament\Resources\OAuthClientResource\Pages;
use App\Filament\Resources\OAuthClientResource;
use Filament\Resources\Pages\ListRecords;
class ListOAuthClients extends ListRecords
{
protected static string $resource = OAuthClientResource::class;
}

View File

@@ -1,12 +0,0 @@
<?php
namespace App\Filament\Resources\OAuthClientResource\Pages;
use App\Filament\Resources\OAuthClientResource;
use Filament\Resources\Pages\ViewRecord;
class ViewOAuthClient extends ViewRecord
{
protected static string $resource = OAuthClientResource::class;
}

View File

@@ -1,200 +0,0 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\RefreshTokenResource\Pages;
use App\Filament\Resources\RefreshTokenResource\RelationManagers\AuditsRelationManager;
use App\Models\RefreshToken;
use BackedEnum;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Table;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Database\Eloquent\Builder;
class RefreshTokenResource extends Resource
{
protected static ?string $model = RefreshToken::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-shield-check';
protected static ?int $navigationSort = 32;
public static function getNavigationGroup(): string
{
return __('admin.nav.security');
}
public static function getNavigationLabel(): string
{
return __('admin.refresh_tokens.menu');
}
public static function getPluralLabel(): string
{
return __('admin.refresh_tokens.menu');
}
public static function getModelLabel(): string
{
return __('admin.refresh_tokens.single');
}
public static function form(Schema $form): Schema
{
return $form->schema([]);
}
public static function table(Table $table): Table
{
return $table
->defaultSort('created_at', 'desc')
->columns([
Tables\Columns\TextColumn::make('tenant.name')
->label(__('admin.refresh_tokens.fields.tenant'))
->searchable(),
Tables\Columns\TextColumn::make('client_id')
->label(__('admin.refresh_tokens.fields.client'))
->copyable()
->toggleable(),
Tables\Columns\TextColumn::make('ip_address')
->label(__('admin.refresh_tokens.fields.ip_address'))
->toggleable(),
Tables\Columns\TextColumn::make('user_agent')
->label(__('admin.refresh_tokens.fields.user_agent'))
->limit(40)
->tooltip(fn (RefreshToken $record) => $record->user_agent)
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('created_at')
->label(__('admin.refresh_tokens.fields.created_at'))
->since()
->sortable(),
Tables\Columns\TextColumn::make('last_used_at')
->label(__('admin.refresh_tokens.fields.last_used_at'))
->since()
->sortable()
->toggleable(),
Tables\Columns\TextColumn::make('expires_at')
->label(__('admin.refresh_tokens.fields.expires_at'))
->dateTime()
->sortable()
->toggleable(),
Tables\Columns\TextColumn::make('status')
->label(__('admin.refresh_tokens.fields.status'))
->badge()
->formatStateUsing(function (RefreshToken $record): string {
if ($record->revoked_at) {
return __('admin.refresh_tokens.status.revoked');
}
if ($record->expires_at && $record->expires_at->isPast()) {
return __('admin.refresh_tokens.status.expired');
}
return __('admin.refresh_tokens.status.active');
})
->color(function (RefreshToken $record): string {
if ($record->revoked_at) {
return 'danger';
}
if ($record->expires_at && $record->expires_at->isPast()) {
return 'warning';
}
return 'success';
}),
Tables\Columns\TextColumn::make('revoked_reason')
->label(__('admin.refresh_tokens.fields.revoked_reason'))
->formatStateUsing(function (?string $state): ?string {
if (! $state) {
return null;
}
$key = "admin.refresh_tokens.reasons.{$state}";
$translated = __($key);
return $translated === $key ? $state : $translated;
})
->badge()
->color('gray')
->toggleable(),
])
->filters([
SelectFilter::make('status')
->label(__('admin.refresh_tokens.filters.status'))
->options([
'active' => __('admin.refresh_tokens.status.active'),
'revoked' => __('admin.refresh_tokens.status.revoked'),
'expired' => __('admin.refresh_tokens.status.expired'),
])
->query(function (Builder $query, array $data): Builder {
return match ($data['value'] ?? null) {
'revoked' => $query->whereNotNull('revoked_at'),
'expired' => $query->whereNull('revoked_at')->whereNotNull('expires_at')->where('expires_at', '<=', now()),
'active' => $query->whereNull('revoked_at')->where(function ($inner) {
$inner->whereNull('expires_at')
->orWhere('expires_at', '>', now());
}),
default => $query,
};
}),
SelectFilter::make('tenant_id')
->label(__('admin.refresh_tokens.filters.tenant'))
->relationship('tenant', 'name')
->searchable(),
])
->actions([
Tables\Actions\ViewAction::make(),
Action::make('revoke')
->label(__('admin.refresh_tokens.actions.revoke'))
->icon('heroicon-o-no-symbol')
->color('danger')
->visible(fn (RefreshToken $record): bool => $record->isActive())
->form([
Forms\Components\Select::make('reason')
->label(__('admin.refresh_tokens.fields.revoked_reason'))
->options([
'manual' => __('admin.refresh_tokens.reasons.manual'),
'operator' => __('admin.refresh_tokens.reasons.operator'),
])
->default('manual')
->required(),
Forms\Components\Textarea::make('note')
->label(__('admin.refresh_tokens.fields.note'))
->rows(2)
->maxLength(255),
])
->requiresConfirmation()
->action(function (RefreshToken $record, array $data): void {
$note = $data['note'] ?? null;
$record->revoke(
$data['reason'] ?? 'manual',
auth()->id(),
request(),
$note ? ['note' => $note] : []
);
}),
])
->bulkActions([]);
}
public static function getRelations(): array
{
return [
AuditsRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListRefreshTokens::route('/'),
'view' => Pages\ViewRefreshToken::route('/{record}'),
];
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Filament\Resources\RefreshTokenResource\Pages;
use App\Filament\Resources\RefreshTokenResource;
use Filament\Resources\Pages\ListRecords;
class ListRefreshTokens extends ListRecords
{
protected static string $resource = RefreshTokenResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Filament\Resources\RefreshTokenResource\Pages;
use App\Filament\Resources\RefreshTokenResource;
use Filament\Actions;
use Filament\Forms;
use Filament\Resources\Pages\ViewRecord;
class ViewRefreshToken extends ViewRecord
{
protected static string $resource = RefreshTokenResource::class;
protected function getHeaderActions(): array
{
return [
Actions\Action::make('revoke')
->label(__('admin.refresh_tokens.actions.revoke'))
->icon('heroicon-o-no-symbol')
->color('danger')
->visible(fn (): bool => $this->record->isActive())
->form([
Forms\Components\Select::make('reason')
->label(__('admin.refresh_tokens.fields.revoked_reason'))
->options([
'manual' => __('admin.refresh_tokens.reasons.manual'),
'operator' => __('admin.refresh_tokens.reasons.operator'),
])
->default('manual')
->required(),
Forms\Components\Textarea::make('note')
->label(__('admin.refresh_tokens.fields.note'))
->rows(2)
->maxLength(255),
])
->requiresConfirmation()
->action(function (array $data): void {
$note = $data['note'] ?? null;
$this->record->revoke(
$data['reason'] ?? 'manual',
auth()->id(),
request(),
$note ? ['note' => $note] : []
);
$this->record->refresh();
$this->notify('success', __('admin.refresh_tokens.notifications.revoked'));
}),
];
}
}

View File

@@ -1,69 +0,0 @@
<?php
namespace App\Filament\Resources\RefreshTokenResource\RelationManagers;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class AuditsRelationManager extends RelationManager
{
protected static string $relationship = 'audits';
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('event')
->defaultSort('created_at', 'desc')
->columns([
Tables\Columns\TextColumn::make('created_at')
->label(__('admin.refresh_tokens.audit.performed_at'))
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('event')
->label(__('admin.refresh_tokens.audit.event'))
->badge()
->formatStateUsing(function (?string $state): ?string {
if (! $state) {
return null;
}
$key = "admin.refresh_tokens.audit.events.{$state}";
$translated = __($key);
return $translated === $key ? $state : $translated;
}),
Tables\Columns\TextColumn::make('performedBy.name')
->label(__('admin.refresh_tokens.audit.performed_by'))
->placeholder('—')
->toggleable(),
Tables\Columns\TextColumn::make('ip_address')
->label(__('admin.refresh_tokens.audit.ip_address'))
->toggleable(),
Tables\Columns\TextColumn::make('context')
->label(__('admin.refresh_tokens.audit.context'))
->formatStateUsing(function ($state): string {
if (! is_array($state) || empty($state)) {
return '—';
}
return collect($state)
->filter(fn ($value) => filled($value))
->map(function ($value, $key) {
if (is_array($value)) {
$value = json_encode($value);
}
return "{$key}: {$value}";
})
->implode(', ');
})
->wrap()
->toggleable(),
])
->filters([])
->paginated([10, 25, 50])
->emptyStateHeading(__('admin.refresh_tokens.audit.empty.heading'))
->emptyStateDescription(__('admin.refresh_tokens.audit.empty.description'));
}
}

View File

@@ -109,6 +109,49 @@ class TenantAdminTokenController extends Controller
]);
}
public function legacyTenantMe(Request $request): JsonResponse
{
/** @var Tenant|null $tenant */
$tenant = $request->attributes->get('tenant')
?? $request->user()?->tenant;
if (! $tenant) {
return response()->json([
'error' => 'tenant_not_found',
'message' => 'Tenant context missing.',
], 404);
}
$tenant->loadMissing('activeResellerPackage');
$user = $request->user();
$abilities = $user?->currentAccessToken()?->abilities ?? [];
$fullName = null;
if ($user) {
$first = trim((string) ($user->first_name ?? ''));
$last = trim((string) ($user->last_name ?? ''));
$fullName = trim($first.' '.$last) ?: null;
}
$activePackage = $tenant->activeResellerPackage;
return response()->json([
'id' => $tenant->id,
'tenant_id' => $tenant->id,
'name' => $tenant->name,
'slug' => $tenant->slug,
'email' => $tenant->contact_email,
'fullName' => $fullName,
'event_credits_balance' => $tenant->event_credits_balance,
'active_reseller_package_id' => $activePackage?->id,
'remaining_events' => $activePackage?->remaining_events ?? 0,
'package_expires_at' => $activePackage?->expires_at,
'features' => $tenant->features ?? [],
'scopes' => $abilities,
]);
}
public function exchange(Request $request): JsonResponse
{
/** @var User|null $user */

View File

@@ -209,7 +209,7 @@ class AuthenticatedSessionController extends Controller
$path = '/'.$path;
}
if (str_starts_with($path, '/event-admin') || str_starts_with($path, '/api/v1/oauth/authorize')) {
if (str_starts_with($path, '/event-admin')) {
return $hasScheme ? $target : $path;
}

View File

@@ -1,39 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class OAuthClient extends Model
{
protected $table = 'oauth_clients';
protected $guarded = [];
protected $fillable = [
'id',
'client_id',
'client_secret',
'tenant_id',
'redirect_uris',
'scopes',
'is_active',
];
protected $casts = [
'id' => 'string',
'tenant_id' => 'integer',
'redirect_uris' => 'array',
'scopes' => 'array',
'is_active' => 'bool',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
}

View File

@@ -1,47 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class OAuthCode extends Model
{
protected $table = 'oauth_codes';
public $timestamps = false;
protected $guarded = [];
protected $fillable = [
'id',
'client_id',
'user_id',
'code',
'code_challenge',
'state',
'redirect_uri',
'scope',
'expires_at',
];
protected $casts = [
'expires_at' => 'datetime',
'created_at' => 'datetime',
];
public function client(): BelongsTo
{
return $this->belongsTo(OAuthClient::class, 'client_id', 'client_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function isExpired(): bool
{
return $this->expires_at < now();
}
}

View File

@@ -1,120 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class RefreshToken extends Model
{
public $incrementing = false;
protected $keyType = 'string';
public $timestamps = false;
protected $table = 'refresh_tokens';
protected $guarded = [];
protected $fillable = [
'id',
'tenant_id',
'client_id',
'token',
'access_token',
'expires_at',
'last_used_at',
'scope',
'ip_address',
'user_agent',
'revoked_at',
'revoked_reason',
];
protected $casts = [
'expires_at' => 'datetime',
'last_used_at' => 'datetime',
'revoked_at' => 'datetime',
'created_at' => 'datetime',
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function audits(): HasMany
{
return $this->hasMany(RefreshTokenAudit::class);
}
public function revoke(?string $reason = null, ?int $performedBy = null, ?Request $request = null, array $context = []): bool
{
$result = $this->update([
'revoked_at' => now(),
'revoked_reason' => $reason,
]);
$event = match ($reason) {
'rotated' => 'rotated',
'ip_mismatch' => 'ip_mismatch',
'expired' => 'expired',
'invalid_secret' => 'invalid_secret',
'tenant_missing' => 'tenant_missing',
'max_active_limit' => 'max_active_limit',
default => 'revoked',
};
$this->recordAudit(
$event,
array_merge([
'reason' => $reason,
], $context),
$performedBy,
$request
);
return $result;
}
public function isActive(): bool
{
if ($this->revoked_at !== null) {
return false;
}
return $this->expires_at === null || $this->expires_at->isFuture();
}
public function scopeActive($query)
{
return $query
->whereNull('revoked_at')
->where(function ($inner) {
$inner->whereNull('expires_at')
->orWhere('expires_at', '>', now());
});
}
public function scopeForTenant($query, string $tenantId)
{
return $query->where('tenant_id', $tenantId);
}
public function recordAudit(string $event, array $context = [], ?int $performedBy = null, ?Request $request = null): void
{
$request ??= request();
$this->audits()->create([
'tenant_id' => $this->tenant_id,
'client_id' => $this->client_id,
'event' => $event,
'context' => $context ?: null,
'ip_address' => $request?->ip(),
'user_agent' => $request?->userAgent(),
'performed_by' => $performedBy,
'created_at' => now(),
]);
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class RefreshTokenAudit extends Model
{
use HasFactory;
protected $fillable = [
'refresh_token_id',
'tenant_id',
'client_id',
'event',
'context',
'ip_address',
'user_agent',
'performed_by',
'created_at',
];
public $timestamps = false;
protected $casts = [
'context' => 'array',
'created_at' => 'datetime',
];
public function refreshToken(): BelongsTo
{
return $this->belongsTo(RefreshToken::class);
}
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function performedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'performed_by');
}
}

View File

@@ -1,51 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TenantToken extends Model
{
public $incrementing = false;
protected $keyType = 'string';
public $timestamps = false;
protected $table = 'tenant_tokens';
protected $guarded = [];
protected $fillable = [
'id',
'tenant_id',
'jti',
'token_type',
'expires_at',
'revoked_at',
];
protected $casts = [
'expires_at' => 'datetime',
'revoked_at' => 'datetime',
'created_at' => 'datetime',
];
public function scopeActive($query)
{
return $query->whereNull('revoked_at')->where('expires_at', '>', now());
}
public function scopeForTenant($query, string $tenantId)
{
return $query->where('tenant_id', $tenantId);
}
public function revoke(): bool
{
return $this->update(['revoked_at' => now()]);
}
public function isActive(): bool
{
return $this->revoked_at === null && $this->expires_at > now();
}
}

View File

@@ -1,38 +0,0 @@
<?php
namespace App\Policies;
use App\Models\OAuthClient;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class OAuthClientPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return $user->role === 'super_admin';
}
public function view(User $user, OAuthClient $oauthClient): bool
{
return $user->role === 'super_admin';
}
public function create(User $user): bool
{
return $user->role === 'super_admin';
}
public function update(User $user, OAuthClient $oauthClient): bool
{
return $user->role === 'super_admin';
}
public function delete(User $user, OAuthClient $oauthClient): bool
{
return $user->role === 'super_admin';
}
}

View File

@@ -128,10 +128,6 @@ class AppServiceProvider extends ServiceProvider
return Limit::perMinute(100)->by($key);
});
RateLimiter::for('oauth', function (Request $request) {
return Limit::perMinute(10)->by('oauth:'.($request->ip() ?? 'unknown'));
});
RateLimiter::for('tenant-auth', function (Request $request) {
return Limit::perMinute(20)->by('tenant-auth:'.($request->ip() ?? 'unknown'));
});

View File

@@ -2,11 +2,9 @@
namespace App\Providers;
use App\Models\OAuthClient;
use App\Models\PurchaseHistory;
use App\Models\Tenant;
use App\Models\User;
use App\Policies\OAuthClientPolicy;
use App\Policies\PurchaseHistoryPolicy;
use App\Policies\TenantPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
@@ -22,7 +20,6 @@ class AuthServiceProvider extends ServiceProvider
protected $policies = [
Tenant::class => TenantPolicy::class,
PurchaseHistory::class => PurchaseHistoryPolicy::class,
OAuthClient::class => OAuthClientPolicy::class,
];
/**
@@ -37,4 +34,3 @@ class AuthServiceProvider extends ServiceProvider
});
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Support;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class TenantAuth
{
@@ -16,24 +15,17 @@ class TenantAuth
*/
public static function resolveAdminUser(Request $request): User
{
$decoded = (array) $request->attributes->get('decoded_token', []);
$tenantId = $request->attributes->get('tenant_id')
?? $request->input('tenant_id')
?? Arr::get($decoded, 'tenant_id');
?? $request->user()?->tenant_id;
if (! $tenantId) {
throw (new ModelNotFoundException)->setModel(User::class);
}
$userId = Arr::get($decoded, 'user_id');
if ($userId) {
$user = User::query()
->whereKey($userId)
->where('tenant_id', $tenantId)
->first();
if ($user) {
$user = $request->user();
if ($user && in_array($user->role, ['tenant_admin', 'admin', 'super_admin'], true)) {
if ($user->role !== 'super_admin' || (int) $user->tenant_id === (int) $tenantId) {
return $user;
}
}