familyHub/includes/persona.php
Louis Whittington f14de0b7e1 Add NFC chore submission feature and enhance family settings
- Introduced NFC support for chore submissions, allowing specific person credit after Head of Household approval.
- Updated family settings to include NFC base URL, scan cooldown, and confirmation page options.
- Enhanced chore management with options for anyone to complete chores and NFC link generation.
- Improved API endpoints for handling NFC tokens and chore submissions.
- Updated readme to reflect new NFC features and settings.
2026-03-31 10:28:27 -05:00

151 lines
3.9 KiB
PHP

<?php
require_once __DIR__ . '/db.php';
const ROLE_HEAD = 'head_of_household';
const ROLE_ADULT = 'adult';
const ROLE_CHILD = 'child';
const SESSION_ACTIVE_PERSON = 'active_person_id';
const SESSION_HOH_VERIFIED = 'hoh_verified';
function startFamilyHubSession(): void {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
function getActivePersonId(): ?string {
$id = $_SESSION[SESSION_ACTIVE_PERSON] ?? null;
return is_string($id) && $id !== '' ? $id : null;
}
function setSessionPerson(string $personId, bool $hohVerified): void {
$_SESSION[SESSION_ACTIVE_PERSON] = $personId;
$_SESSION[SESSION_HOH_VERIFIED] = $hohVerified;
}
function clearPersonaSession(): void {
unset($_SESSION[SESSION_ACTIVE_PERSON], $_SESSION[SESSION_HOH_VERIFIED]);
}
function isHohVerified(): bool {
return !empty($_SESSION[SESSION_HOH_VERIFIED]);
}
function findPersonById(array $people, string $id): ?array {
foreach ($people as $p) {
if (($p['id'] ?? '') === $id) {
return $p;
}
}
return null;
}
function getActivePerson(array $people): ?array {
$id = getActivePersonId();
if ($id === null) {
return null;
}
return findPersonById($people, $id);
}
function personRequiresPinToActivate(?array $person): bool {
if ($person === null) {
return false;
}
return ($person['role'] ?? '') === ROLE_HEAD && !empty($person['pin_hash']);
}
/**
* True when at least one person is recorded as Head of household (setup complete).
*/
function familyHubHasHeadOfHousehold(array $people): bool {
foreach ($people as $p) {
if (($p['role'] ?? '') === ROLE_HEAD) {
return true;
}
}
return false;
}
function assertHoHCanManagePeople(array $people): void {
if (count($people) === 0) {
return;
}
$active = getActivePerson($people);
if ($active === null || ($active['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Head of household verification required.']);
exit;
}
}
/**
* @param mixed $raw
* @return array<int, array<string, mixed>>
*/
function normalizePeopleList($raw): array {
if (!is_array($raw) || !array_is_list($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (is_array($row) && !empty($row['id']) && is_string($row['id'])) {
$out[] = $row;
}
}
return $out;
}
/**
* @param array<string, mixed> $person
* @return array<string, mixed>
*/
function migrateLegacyPersonRow(array $person): array {
if (!array_key_exists('nfc_submit_token_hash', $person) || !is_string($person['nfc_submit_token_hash'])) {
$person['nfc_submit_token_hash'] = '';
}
if (!array_key_exists('nfc_submit_token_updated_at', $person) || !is_string($person['nfc_submit_token_updated_at'])) {
$person['nfc_submit_token_updated_at'] = '';
}
return $person;
}
/**
* @param array<int, array<string, mixed>> $people
* @return array<int, array<string, mixed>>
*/
function migrateAllPeople(array $people): array {
$out = [];
foreach ($people as $person) {
if (!is_array($person)) {
continue;
}
$out[] = migrateLegacyPersonRow($person);
}
return $out;
}
/**
* @param array<int, array<string, mixed>> $people
* @return array{index:int, person:array<string, mixed>}|null
*/
function findPersonBySubmitToken(array $people, string $token): ?array {
$token = trim($token);
if ($token === '') {
return null;
}
$candidateHash = hash('sha256', $token);
foreach ($people as $index => $person) {
$storedHash = trim((string) ($person['nfc_submit_token_hash'] ?? ''));
if ($storedHash === '') {
continue;
}
if (hash_equals($storedHash, $candidateHash)) {
return ['index' => (int) $index, 'person' => $person];
}
}
return null;
}