Closes the P2 items from /tmp/service/new/01-TZ-rbac §4.1 §4.2.
== User invitation workflow ==
New columns on users: invited_at, invited_by_id (FK self), accepted_at,
invitation_token (sha256 hash, indexed). Migration is idempotent.
User::sendInvitation($invitedBy = auth()->user())
- generates 64-char random token
- stores sha256(token) in invitation_token column (never plaintext)
- marks invited_at = now(), status = inactive
- queues UserInvitationMail to the user's email with the signed accept URL
- returns the raw token (for tests / API consumers)
User::findByInvitationToken($rawToken) hashes + lookups.
User::acceptInvitation($password) sets password (hashed cast), clears
invitation_token, marks accepted_at + email_verified_at, status = active.
Web routes (no auth — token IS the credential):
GET /invitations/{token} → password-set form
POST /invitations/{token} → validates min:8 + confirmed, accepts
Tokens expire after 7 days (checked against invited_at). Expired and
invalid tokens render dedicated views (invitations/expired.blade.php,
invitations/invalid.blade.php) instead of generic 404 — so the user
knows to ask for a resend.
UserInvitationMail uses Filament's existing markdown layout; subject
includes the tenant display_name.
== REST API ==
Twenty new endpoints under /api/v1/ (Sanctum auth + tenant scoping
via the existing EnsureTokenMatchesTenant middleware). All gated by
ADMIN_USERS_* / ADMIN_ROLES_MANAGE permissions; mechanic-level token
gets 403.
Users:
GET /users — paginated + role/status/q filters
GET /users/{u} — eager-loads roles + overrides + invitedBy
POST /users — creates inactive user + sends invitation
PATCH /users/{u} — update name/email/role/status
DELETE /users/{u} — soft delete
POST /users/{u}/activate
POST /users/{u}/deactivate — also revokes all sessions
POST /users/{u}/resend-invitation
POST /users/{u}/force-password-reset — re-sends invitation
GET /users/{u}/sessions — list active sessions (from sessions table)
DELETE /users/{u}/sessions — revoke all
DELETE /users/{u}/sessions/{sessionId} — revoke one
GET /users/{u}/roles — assigned roles
POST /users/{u}/roles — assign role
DELETE /users/{u}/roles/{role} — remove role
GET /users/{u}/permissions — effective: role perms + grants - active denies
POST /users/{u}/permission-overrides — add grant/deny (with optional expires_at)
DELETE /users/{u}/permission-overrides/{perm}
Roles:
apiResource roles — index/show/store/update/destroy
(system roles guarded against rename/delete)
GET /roles/{r}/permissions
PUT /roles/{r}/permissions — bulk sync
GET /permissions — catalog: flat list + grouped + labels + role labels
Authorization is uniform: every controller method calls $this->authorize()
which throws 403 if canDo(perm) is false. canDo() already honors the
overrides + admin bypass + audit log from earlier commits, so the API
behaves identically to the Filament UI.
== Tests ==
InvitationFlowTest (8): token generation + sha256 storage + queued mail,
findByInvitationToken happy/sad path, accept sets password + activates,
GET form renders, POST accepts + redirects, invalid token view,
backdated invited_at → expired view, password too short → validation error.
RbacApiTest (12): admin can list users, mechanic 403, create user
queues invitation, assign+remove role round-trip, effective permissions
endpoint subtracts active denies, add+remove override via API,
role index returns 7 system roles with permission counts (51 for owner),
role sync permissions, system role destroy rejected with 422,
permission catalog endpoint returns all 51 + grouped + labels,
revoke all sessions deletes only target user's rows.
Suite: 234 passed (659 assertions). Was 214.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shop password reset:
- Configured 'shop_customers' password broker on the existing
password_reset_tokens table
- ShopCustomer::sendPasswordResetNotification overrides Laravel default to
send a ShopPasswordResetMail with a tenant-subdomain reset URL
- Routes /shop/password/forgot, /shop/password/email, /shop/password/reset/{token}
+ ShopAuthController showForgotPassword/sendResetLink/showResetPassword/
resetPassword. Forgot view stays generic ("if it exists, we sent…") to avoid
email enumeration. Login view links to "Am uitat parola".
Order confirmation email:
- ShopOrderConfirmationMail + nicely formatted HTML email template
- ShopOrderNotifier::placed now also emails customer_email (best-effort,
warning-only logged on failure) alongside existing Telegram + staff push
Multiple images per Part:
- Part media collection switched from singleFile to multiple (max 8 in form)
- imageUrls() helper for galleries; imageUrl() still returns first for cards
- PartResource form: reorderable multi-upload
- Shop part detail: vertical thumbnails switch the main image via vanilla JS
ShopCustomerResource (tenant Filament, "Magazin" nav group):
- List with name/phone/email/client_id/orders_count/last_login_at
- Edit (no password field exposed)
- "Trimite reset parolă" action uses the new broker
- OrdersRelationManager shows the customer's orders read-only
Tests (7 new):
- forgot sends mail; forgot doesn't disclose unknown email; reset with valid
token changes password; bad token rejected; order email when customer_email
set; email skipped without it; Part has imageUrls() collection
Full suite: 130 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Models & migrations:
- platform_settings table (key/value JSON store + Cache::remember 5min)
- plans: is_demo bool + trial_days int
- companies: is_demo bool
Plans:
- Demo plan seeded (is_demo=true, is_public=false, all features, 14 trial days)
- Trial 14-day plan seeded (is_public=true, basic features)
- Plan form: is_demo toggle + trial_days field
- Plan table: badge 🎬 Demo / 🎁 N zile trial
Central panel:
- PaymentSettings page (heroicon-credit-card, sort 90)
Form sections: General, Date legale, Stripe, PayPal, Transfer bancar
Each gateway collapsible, fields hidden until enabled toggle
Saves to platform_settings keyed by `payments.{gateway}`
- CompanyResource: is_demo toggle + table description
Payment flow (PaymentController):
- GET /billing — tenant invoices list with Pay button
- POST /pay/{sub} — start checkout (stripe/paypal/bank)
- GET /pay/{sub}/{success,cancel}
- POST /payments/stripe/webhook — mark paid + extend company.active_until
- POST /payments/paypal/webhook — same
Views:
- site/billing.blade.php — invoices list with payment modal (3 methods)
- site/bank-instructions — IBAN/BIC/reference for manual transfer
- site/checkout-stub — placeholder until composer require stripe-php
- site/payment-{success,cancel}
Tenant panel:
- userMenuItems → "Facturile mele" link to /billing