Resources
6Install
npx skillscat add arqel-dev/auth Install via the SkillsCat registry.
SKILL.md — arqel-dev/auth
Contexto canónico para AI agents. Estrutura conforme
PLANNING/04-repo-structure.md§11.
Purpose
arqel-dev/auth é a fina camada de authorization (não authentication) que envolve Laravel Policies + Gate com conveniences específicas para Arqel:
PolicyDiscovery— verifica/auto-registra Policies para Resources, com warning quando ausentes e suporte a override via$policyestáticoAbilityRegistry— catálogo de abilities globais (resolvidas via Gate) + computed (closures arbitrárias) que são serializadas em shared props (auth.can.*) para o lado ReactArqelGate— facade conveniente sobre Laravel Gate integrada com oAbilityRegistry
User escreve as Policies (Laravel-native). Arqel apenas verifica existência, auto-registra com Gate::policy(...) e resolve abilities globais por user.
Inertia pages renderizadas
| Controller | Component (Inertia) | Pacote npm |
|---|---|---|
LoginController |
arqel-dev/auth/Login |
@arqel-dev/auth |
RegisterController |
arqel-dev/auth/Register |
@arqel-dev/auth |
ForgotPasswordController |
arqel-dev/auth/ForgotPassword |
@arqel-dev/auth |
ResetPasswordController |
arqel-dev/auth/ResetPassword |
@arqel-dev/auth |
EmailVerificationController |
arqel-dev/auth/VerifyEmailNotice |
@arqel-dev/auth |
Os componentes vivem em @arqel-dev/auth (npm) e são instalados automaticamente pelo arqel:install.
Authentication (login/registro/forgot-password) não está incluída neste pacote. Decisão de design: Arqel hoje delega ao starter kit Laravel (Breeze/Jetstream/Fortify) — o
arqel newCLI instala Breeze + React + Inertia por default. Para apps que rodaram sócomposer require arqel-dev/framework, é necessário instalar manualmente um starter kit. Verapps/docs/guide/authentication.md. Tickets AUTH-006/007/008 (TBD) preveem shipar páginas Inertia-React opt-in dentro deste pacote, equivalente ao que Filament/Nova oferecem out-of-the-box.
Status
Entregue (AUTH-001..004):
Arqel\Auth\AuthServiceProvider— auto-discovery, registaAbilityRegistryePolicyDiscoverycomo singletonsArqel\Auth\AbilityRegistry—registerGlobal/Globals/Computed,resolveForUsercom cache per-request,clearArqel\Auth\PolicyDiscovery—autoRegisterPoliciesFor(array $resources)retorna{registered, missing}. Heurística\Models\→\Policies\. HonraResource::$policyoverrideArqel\Auth\ArqelGate— facade comregister/abilities/allows/denies/snapshotintegrada aoAbilityRegistryArqel\Auth\Concerns\AuthorizesRequeststrait com 3 oracles:authorizeResource(class, action, ?record),authorizeAction(action, ?record),authorizeField(field, operation, ?record). Aborta 403 quando o gate denies; silently allow quando não há policy registadaArqel\Auth\Http\Middleware\EnsureUserCanAccessPanel— middleware com ability configurável (defaultviewAdminPanel). Aborta 401 para guests, 403 quando o gate denies, allow-through quando a ability não está registadaarqel_can(string, mixed)global helper:AbilityRegistrysnapshot first, Gate fallback. Retorna false para guests- 28 testes Pest passando
Entregue (AUTH-008):
Arqel\Auth\Http\Controllers\ForgotPasswordController— GET renderiza Inertiaarqel-dev/auth/ForgotPassword; POST valida e-mail, disparaPassword::sendResetLink, retorna flashstatusgenérico (não revela se e-mail existe). Rate-limit 3/email+IP/horaArqel\Auth\Http\Controllers\ResetPasswordController— GET renderizaarqel-dev/auth/ResetPasswordcom{token, email}; POST valida viaResetPasswordRequeste processaPassword::reset. Sucesso redireciona paraPanel::getLoginUrl()Arqel\Auth\Http\Requests\ResetPasswordRequest— rulestoken+email+password(min:8 confirmed)+password_confirmation; rate-limit 3/email+IP/horaRoutes::registerPasswordReset(?Panel)— registrapassword.request,password.email,password.reset,password.update(idempotente; pula se host já tempassword.request/password.reset)Panel::passwordReset()/passwordResetEnabled()/passwordResetExpirationMinutes()/forgotPasswordUrl()— fluent API opt-in.passwordResetExpirationMinutesajustaauth.passwords.users.expireem runtime- Pacote npm
@arqel-dev/authganha<ForgotPasswordPage />e<ResetPasswordPage />
Exemplo:
$panel
->login()
->passwordReset()
->passwordResetExpirationMinutes(120);Entregue (AUTH-006):
Arqel\Auth\Http\Controllers\LoginController— GET renderiza Inertiaarqel-dev/auth/LoginpassandologinUrl,registerUrleforgotPasswordUrlcomo props (todos respeitamPanel::path()actual;registerUrleforgotPasswordUrlsãonullquando o feature respectivo não está activado, permitindo ao componente React esconder os links). POST autentica viaLoginRequest, regenera sessão e redireciona paraPanel::getAfterLoginUrl()Arqel\Auth\Http\Controllers\LogoutController— invalida sessão, rotaciona CSRF, redireciona paraPanel::getLoginUrl()Arqel\Auth\Http\Requests\LoginRequest— rate-limit Laravel-native (5/min por email+IP), disparaLockouteventArqel\Auth\Routes::register(?Panel)— registo idempotente. Quando o?Panelé omitido, auto-detecta o panel viaPanelRegistry::getCurrent()desde que o panel tenha->login()activo. Pula quando o host já tem rota nomeadalogin(Breeze/Jetstream/Fortify) para coexistir com starter kits.Arqel\Auth\Routes::registerLogout(?Panel)— deriva a URL de logout viaderiveLogoutUrl($loginUrl)(substitui o último segmento do path por/logoutmantendo o prefixo do panel). Idempotente.Panel::login()/loginUrl()/afterLoginRedirectTo()/registration()/withoutDefaultAuth()/loginEnabled()/registrationEnabled()— fluent API opt-in- Pacote npm
@arqel-dev/authcom componente<LoginPage />Inertia-React
Entregue (AUTH-007):
Arqel\Auth\Http\Controllers\RegisterController— GET renderiza Inertiaarqel-dev/auth/Register; POST cria User viaconfig('auth.providers.users.model'), disparaRegisteredevent, faz auto-login e redirecionaArqel\Auth\Http\Requests\RegisterRequest— rules name/email/password comconfirmed, rate-limit 3 registros/IP/horaArqel\Auth\Http\Controllers\EmailVerificationController—notice(Inertia notice page),verify(signed URL handler que disparaVerified),resend(reenvio com flash status)Arqel\Auth\Routes::registerRegistration()eregisterEmailVerification()— registos idempotentes, opt-in viaPanel::registration()ePanel::emailVerification()Panel::emailVerification()/emailVerificationEnabled()/registrationFields()/getRegistrationFields()— fluent API opt-in- Componentes npm
<RegisterPage />e<VerifyEmailNoticePage />em@arqel-dev/auth - Reservou
email/noroutes/arqel.php$reservedSlugspara não colidir com{resource}polymórfico
Exemplo de uso:
$panel = Panel::configure()
->login()
->registration()
->emailVerification();Por chegar:
- Integração com
arqel-dev/coreArqelServiceProvider::packageBootedpara chamarPolicyDiscovery::autoRegisterPoliciesFor(ResourceRegistry::all())automaticamente (AUTH-005 wrap-up)
Key Contracts
AbilityRegistry
$registry = app(AbilityRegistry::class);
$registry
->registerGlobal('viewAdminPanel') // resolvido via Gate::allows
->registerGlobals(['manageSettings', 'exportData'])
->registerComputed('isPremium', fn ($user) => $user?->subscription?->isPremium());
// Inertia middleware (CORE-007) chama:
$can = $registry->resolveForUser(auth()->user());
// → ['viewAdminPanel' => true, 'manageSettings' => false, 'exportData' => true, 'isPremium' => true]Cache per-request por getAuthIdentifier() ('guest' para null) — múltiplas chamadas para o mesmo user dentro do mesmo request não re-executam Gate/closures.
PolicyDiscovery
$discovery = app(PolicyDiscovery::class);
$result = $discovery->autoRegisterPoliciesFor([
UserResource::class,
PostResource::class,
]);
// $result['registered']: ['App\Models\User' => 'App\Policies\UserPolicy', ...]
// $result['missing']: [class-string, ...] — emits log warning per resourceHeurística:
- Se Resource tem
public static ?string $policy = X::class, usa X (se existir). - Caso contrário, troca
\Models\por\Policies\no namespace do model e adiciona sufixoPolicy. - Se resultado não existe, adiciona ao
missing[]e emite log warning.
Resources que lançam exceção ao chamar getModel() (i.e. $model não definido) são skippados graciosamente.
ArqelGate
$gate = app(ArqelGate::class);
$gate->register('isPremium', fn ($user) => /* ... */);
$gate->abilities('viewAdminPanel', 'manageSettings');
$gate->allows('viewAdminPanel'); // current user via Auth::user()
$gate->denies('viewAdminPanel', $blogPost); // com argumento
$can = $gate->snapshot(); // === $registry->resolveForUser(Auth::user())Conventions
declare(strict_types=1)obrigatório- User escreve as Policies (Laravel
App\Policies\FooPolicy); Arqel apenas verifica e regista - Global abilities são para flags panel-level (e.g. "user pode entrar no admin", "user pode exportar"); per-record authorization sempre vai por Policies
- Computed abilities são para checks arbitrários (e.g. "user tem subscription paid") — closures recebem
?Authenticatable $usere retornam bool - Cache de abilities é per-request — não persiste entre requests; isto é deliberado para refletir mudanças em real-time
Anti-patterns
- ❌ DB queries pesadas em
registerComputed— closures correm por request por user. Se precisares de N+1 protection, faz eager-load no middleware antes deresolveForUser - ❌ Reescrever Policies como abilities — Policies são canónicas para per-record (Filament-style
view($user, $post)); abilities são para flags panel-level ('viewAdminPanel') - ❌
Gate::policy()manual no AppServiceProvider quando o Resource já está registado — deixaPolicyDiscoverycuidar; evita duplicação - ❌ Override
$policyapontando para classe inexistente —PolicyDiscoveryfazclass_existse ignora silenciosamente; resulta em "missing policy" warnings sem causa óbvia
Related
- Tickets: `PLANNING/08-fase-1-mvp.md` §AUTH-001..005
- ADRs: ADR-001 Inertia-only · ADR-008 Pest 3
- API: `PLANNING/05-api-php.md` §Auth
- Source: `packages/auth/src/`
- Tests: `packages/auth/tests/`