• Mahmoud Aglan's avatar
    feat(portal): identity, the member portal shell, and the check-in pass · acca60b0
    Mahmoud Aglan authored
    S3, S4 and the core of S8.
    
    Identity (S3)
    -------------
    GuardianResolver replaces ten hand-copied
    `Guardian::where('person_id', …)->first()` lookups, every one wrong in the
    same two ways: `->first()` on a column with no unique constraint, so a
    guardian holding two rows saw one set of children and was 403'd on the rest,
    silently; and no answer at all for an adult member, because all eleven
    app/Livewire/Parent/* components end in ->firstOrFail() and a player has no
    guardian row. That is why a player given the `parent` role saw empty lists —
    the domain had no path from a user to his own participant.
    
    PermissionService::getChildParticipantIds() also carried
    `->where('person_id', …)->orWhere('user_id', …)`, which with the tenant
    global scope appended compiles to `person_id = ? OR (user_id = ? AND
    academy_id = ?)` — the first branch escaping the tenant filter entirely. The
    closure is what keeps both branches inside it.
    
    A real `player` role and the portal.* permissions ship as a guarded
    migration, not a seeder: db:seed only runs when RUN_SEED_ON_FIRST_DEPLOY is
    true, so a client deployed outside the one-click template would never receive
    them. Same pattern as 2026_09_01_000001.
    
    portal_invitations stores only the SHA-256 of its token — a raw token in a
    row is a password in a row — and consumption is one conditional UPDATE whose
    WHERE clause carries every condition, so two taps on the same link on a phone
    cannot both create an account. Activation lives in a plain controller, never
    Livewire: a single-use token in a public property is serialised into the page
    on every round-trip.
    
    users.email stays NOT NULL UNIQUE, deliberately. 2024_01_01_000002 declares
    it inside Schema::create, so Postgres emits a UNIQUE CONSTRAINT that cannot
    be made partial without a DROP CONSTRAINT in up(); CREATE INDEX CONCURRENTLY
    cannot run in a migration transaction; and password_reset_tokens.email is the
    primary key the broker keys on. Portal accounts get p{uuid}@portal.invalid
    (RFC 2606, never routable) plus an email_is_synthetic flag every mail path
    checks. No unique index on users.phone either: 2026_08_30_000004 logged that
    it left duplicates in place, so one would hard-fail on at least one live
    client and then block that client's migrations forever.
    
    Phone login now refuses when one number matches several different people —
    signing someone into a stranger's account — while still resolving a genuine
    duplicate pair for the same person.
    
    config/branch_lock.php gains portal.* and parent.*: RequireBranchSelection
    runs on the whole web group, so without it any user holding branches.view_all
    in all-branches mode is bounced out of the portal by middleware.
    
    The portal (S4)
    ---------------
    Five tabs at /app — الرئيسية, التدريب, المدفوعات, الأكاديمية, حسابي — with
    the pass as a header affordance because it is per active profile: a guardian
    with three children needs three.
    
    PortalContext is the scope rule the IA turns on, decided once instead of
    eleven times: training is member-scoped, money is family-scoped. The old
    components each re-read session('active_child_id') independently while
    ParentFinances ignored it and aggregated everyone — the domain saying out
    loud that a household has one balance. The active id is re-validated against
    GuardianResolver on every read, so a value put into the session, or left
    there after a withdrawal, cannot widen what an account sees.
    
    No participant id is held in a public property anywhere in the namespace.
    This is Livewire v4, where a plain public property is settable from the
    browser, so a check in mount() that is not repeated in render() is
    decoration, not a check.
    
    portal.css is the only entrypoint built with `source(none)`. app.css and
    website.css are each a bare `@import 'tailwindcss'`, so v4 auto-detects from
    the project root and both emit the identical complete utility set — a third
    file written the same way would have been a third identical copy. Measured:
    portal.css 17.11 kB / 4.54 kB gzipped against app.css at 208 kB / 31 kB.
    
    Screens surface what was always one join away and never loaded: the coach
    taking each session and the reason for a substitution, cancelled_reason so an
    empty week does not read the same as Eid, and per-event registration for the
    right child — answerable only since event_registrations gained participant_id
    in S1.
    
    The check-in pass (S8 core)
    ---------------------------
    qr_check_in_enabled has been a toggle in system settings with zero functional
    readers since 2026_07_27: the product advertised a feature that did not exist.
    
    The pass asserts identity and never authorizes. Enrolment, participant
    status, session existence and branch are fresh reads at every scan, which is
    what makes a suspension take effect at the next scan rather than the next
    token rotation. The secret is derived by HKDF from a pepper that is
    deliberately not APP_KEY, revocation is one integer column, and a scanned
    code is consumed by INSERT … ON CONFLICT DO NOTHING inside the same
    transaction as the attendance write — a Cache::has/put pair would be a
    time-of-check race, and two scanners at one gate is exactly when it loses.
    Relay is not solvable; it is made worthless instead.
    
    SelfCheckInService writes through AttendanceMarkingService with the scanning
    staff as the marker rather than adding a second attendance write path. The
    deleted API had one of those: POST /v1/absences/report wrote status='excused'
    with no marker, no transition check, no audit and no check that the session
    belonged to the participant.
    
    QrCode is written rather than pulled in — there is no Composer step here that
    can add to the committed lock file, and the alternative was the existing
    pattern of an <img> pointing at api.qrserver.com, which sends the member's
    token to a third party and fails when the venue's wifi does.
    
    It was verified module-for-module against an independent implementation
    across versions 1-10 and all eight masks, given identical codewords. That
    found two bugs neither visible nor throwing: a Reed-Solomon generator
    polynomial built with its terms reversed, and missing version-information
    blocks for versions 7 and up, whose 36 modules were being filled with payload
    and shifting the whole stream. Both produced a plausible square of black and
    white that no scanner accepts. tests/Fixtures/qr_golden.php freezes that
    verification.
    
    Verified against a restored copy of backups/oc_sport-20260831-081053.dump:
    all seven portal screens render 200 for a real member account, the manifest
    is tenant-branded and no-store, and a member opening another family's invoice
    gets 403.
    
    Suite: 76 passed, 3 skipped (the tenant smoke test skips off Postgres rather
    than pretending SQLite is production).
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    acca60b0
Name
Last commit
Last update
..
2024_01_01_000001_create_academies_table.php Loading commit data...
2024_01_01_000002_create_users_table.php Loading commit data...
2024_01_01_000003_create_cache_and_jobs_tables.php Loading commit data...
2024_01_01_000004_create_branches_table.php Loading commit data...
2024_01_01_000005_create_roles_and_permissions_tables.php Loading commit data...
2024_01_01_000006_create_people_table.php Loading commit data...
2024_01_01_000007_create_guardians_table.php Loading commit data...
2024_01_01_000008_add_phase1_columns_to_users_table.php Loading commit data...
2024_01_01_000009_create_login_history_table.php Loading commit data...
2024_01_01_000010_create_financial_accounts_table.php Loading commit data...
2024_01_01_000011_create_invoices_table.php Loading commit data...
2024_01_01_000012_create_payments_table.php Loading commit data...
2024_01_01_000013_create_transactions_table.php Loading commit data...
2024_01_01_000014_create_wallets_table.php Loading commit data...
2024_01_01_000015_create_payment_plans_table.php Loading commit data...
2024_01_01_000016_create_activities_table.php Loading commit data...
2024_01_01_000017_create_participants_table.php Loading commit data...
2024_01_01_000018_create_guardian_participant_table.php Loading commit data...
2024_01_01_000019_create_training_programs_table.php Loading commit data...
2024_01_01_000020_create_training_groups_table.php Loading commit data...
2024_01_01_000021_create_training_schedules_table.php Loading commit data...
2024_01_01_000022_create_training_sessions_table.php Loading commit data...
2024_01_01_000023_create_enrollments_table.php Loading commit data...
2024_01_01_000024_create_waitlists_table.php Loading commit data...
2024_01_01_000025_create_holidays_table.php Loading commit data...
2024_01_01_000026_create_cash_sessions_table.php Loading commit data...
2024_01_01_000027_create_facilities_table.php Loading commit data...
2024_01_01_000028_create_space_layouts_and_segments_table.php Loading commit data...
2024_01_01_000029_create_space_reservations_table.php Loading commit data...
2024_01_01_000030_create_assignments_table.php Loading commit data...
2024_01_01_000031_create_attendance_records_table.php Loading commit data...
2024_01_01_000032_create_base_prices_table.php Loading commit data...
2024_01_01_000033_create_pricing_rules_table.php Loading commit data...
2024_01_01_000034_create_promotions_and_coupons_table.php Loading commit data...
2024_01_01_000035_create_pos_transactions_table.php Loading commit data...
2024_01_01_000036_create_products_and_categories_table.php Loading commit data...
2024_01_01_000037_create_warehouses_and_inventory_levels_table.php Loading commit data...
2024_01_01_000038_create_inventory_movements_table.php Loading commit data...
2024_01_01_000039_create_purchase_orders_table.php Loading commit data...
2024_01_01_000040_create_kits_table.php Loading commit data...
2024_01_01_000041_create_stock_counts_table.php Loading commit data...
2024_01_01_000042_create_notifications_system_table.php Loading commit data...
2024_01_01_000043_create_audit_logs_table.php Loading commit data...
2024_01_01_000044_create_evaluations_table.php Loading commit data...
2024_01_01_000045_create_system_settings_table.php Loading commit data...
2024_01_01_000046_add_monthly_rental_cost_to_facilities.php Loading commit data...
2024_01_01_000047_create_receipt_templates_and_branch_settings.php Loading commit data...
2024_01_01_000048_add_service_fee_to_transactions_and_invoices.php Loading commit data...
2024_01_01_000049_add_branch_id_to_participants_and_payments.php Loading commit data...
2024_01_01_000050_simplify_membership_type_and_add_membership_id.php Loading commit data...
2024_01_01_000051_add_cash_session_id_to_payments.php Loading commit data...
2024_01_01_000052_rename_program_id_to_training_program_id_on_training_groups.php Loading commit data...
2024_01_01_000053_create_employees_table.php Loading commit data...
2024_01_01_000054_create_trainers_table.php Loading commit data...
2024_01_01_000055_create_trainer_qualifications_table.php Loading commit data...
2024_01_01_000056_create_trainer_availability_table.php Loading commit data...
2024_01_01_000057_create_documents_table.php Loading commit data...
2024_01_01_000060_alter_trainers_make_employee_nullable.php Loading commit data...
2024_01_01_000061_create_trainer_compensations_table.php Loading commit data...
2024_01_01_000062_create_payroll_periods_table.php Loading commit data...
2024_01_01_000063_create_payslips_table.php Loading commit data...
2024_01_01_000064_create_payslip_items_table.php Loading commit data...
2024_01_01_000065_create_trainer_advances_table.php Loading commit data...
2024_01_01_000066_create_trainer_rate_history_table.php Loading commit data...
2024_01_01_000067_add_label_ar_to_system_settings_table.php Loading commit data...
2024_01_01_000070_create_media_table.php Loading commit data...
2024_01_01_000071_add_default_trainer_to_training_programs.php Loading commit data...
2024_01_01_000071_create_website_settings_table.php Loading commit data...
2024_01_01_000072_create_website_sections_table.php Loading commit data...
2024_01_01_000073_create_website_testimonials_table.php Loading commit data...
2024_01_01_000074_create_website_faqs_table.php Loading commit data...
2024_01_01_000075_create_website_news_table.php Loading commit data...
2024_01_01_000076_create_website_partners_table.php Loading commit data...
2024_01_01_000077_create_contact_submissions_table.php Loading commit data...
2024_01_01_000078_create_whatsapp_messages_table.php Loading commit data...
2024_01_01_000079_add_billing_fields_to_programs_and_enrollments.php Loading commit data...
2024_01_01_000080_add_governorate_to_people_table.php Loading commit data...
2026_07_16_162146_add_is_essential_to_products_table.php Loading commit data...
2026_07_16_180000_add_delivery_tracking_to_invoice_items.php Loading commit data...
2026_07_16_200000_add_annual_billing_to_products_table.php Loading commit data...
2026_07_16_210000_add_installment_amounts_to_product_installment_plans.php Loading commit data...
2026_07_17_000001_widen_training_groups_code_column.php Loading commit data...
2026_07_22_000001_create_facility_rent_payments_table.php Loading commit data...
2026_07_22_000002_create_expenses_table.php Loading commit data...
2026_07_22_000003_add_expense_permissions.php Loading commit data...
2026_07_24_000001_create_events_table.php Loading commit data...
2026_07_24_000002_create_event_registrations_table.php Loading commit data...
2026_07_24_000003_add_event_media_collections.php Loading commit data...
2026_07_24_000004_add_event_permissions.php Loading commit data...
2026_07_24_100001_add_locations_to_events.php Loading commit data...
2026_07_24_100002_default_website_published_true.php Loading commit data...
2026_07_25_000001_add_print_count_to_invoices_and_payments.php Loading commit data...
2026_07_25_000001_website_builder_v2_phase_a.php Loading commit data...
2026_07_25_100001_website_builder_v2_phase_b.php Loading commit data...
2026_07_25_200001_website_builder_v2_phase_c.php Loading commit data...
2026_07_26_000001_add_events_section_key.php Loading commit data...
2026_07_27_000001_create_device_tokens_table.php Loading commit data...
2026_07_27_000002_add_channel_push_to_notification_preferences.php Loading commit data...
2026_07_27_000003_create_contact_messages_table.php Loading commit data...
2026_07_27_000004_seed_mobile_app_system_settings.php Loading commit data...
2026_07_27_000005_create_service_requests_table.php Loading commit data...
2026_07_27_100001_create_push_announcements_table.php Loading commit data...
2026_07_27_100002_create_push_analytics_table.php Loading commit data...
2026_07_28_000001_drop_membership_id_unique_index.php Loading commit data...
2026_08_02_000001_add_partial_payment_to_products_table.php Loading commit data...
2026_08_02_000002_add_partially_paid_to_pos_payment_status.php Loading commit data...
2026_08_09_000001_add_external_revenue_to_expenses_category.php Loading commit data...
2026_08_09_000002_add_collected_by_to_invoices.php Loading commit data...
2026_08_10_000001_add_preferred_branch_to_users.php Loading commit data...
2026_08_10_000002_add_status_to_expenses.php Loading commit data...
2026_08_13_000001_enable_partial_payment_all_products.php Loading commit data...
2026_08_14_000001_add_attachment_to_expenses.php Loading commit data...
2026_08_19_000001_add_is_free_to_participants.php Loading commit data...
2026_08_30_000001_pricing_discount_overhaul.php Loading commit data...
2026_08_30_000002_seed_pricing_discount_settings.php Loading commit data...
2026_08_30_000003_facility_activities_and_default_layouts.php Loading commit data...
2026_08_30_000004_normalize_user_credentials.php Loading commit data...
2026_08_31_000001_add_attachment_metadata_to_expenses.php Loading commit data...
2026_08_31_000001_create_website_pages_and_blocks.php Loading commit data...
2026_08_31_000001_trim_stray_whitespace_from_names.php Loading commit data...
2026_08_31_000002_add_subject_to_contact_submissions.php Loading commit data...
2026_08_31_000003_create_website_menus.php Loading commit data...
2026_09_01_000001_add_branches_view_all_permission.php Loading commit data...
2026_09_01_000002_backfill_pos_invoice_item_types.php Loading commit data...
2026_09_01_000003_create_program_products_table.php Loading commit data...
2026_09_01_000004_bundle_registration_product_to_team_programs.php Loading commit data...
2026_09_01_000005_revoke_mobile_api_tokens.php Loading commit data...
2026_09_02_000001_backfill_freetext_product_invoice_items.php Loading commit data...
2026_09_03_000001_seed_financial_accounts_for_every_academy.php Loading commit data...
2026_09_03_000002_branch_attribution_for_money.php Loading commit data...
2026_09_03_000003_tenancy_invariant_repairs.php Loading commit data...
2026_09_03_000004_reconcile_check_vocabularies.php Loading commit data...
2026_09_03_000005_create_invoice_number_counters.php Loading commit data...
2026_09_03_000006_repair_mobile_invoice_item_totals.php Loading commit data...
2026_09_04_000001_branding_single_source_of_truth.php Loading commit data...
2026_09_05_000001_add_player_role_and_portal_permissions.php Loading commit data...
2026_09_05_000002_create_portal_invitations.php Loading commit data...
2026_09_06_000001_add_channel_to_published_content.php Loading commit data...
2026_09_07_000001_create_checkin_infrastructure.php Loading commit data...