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.
This commit is contained in:
2026-03-31 10:28:27 -05:00
parent 9a78c4564e
commit f14de0b7e1
21 changed files with 779 additions and 21 deletions
+52
View File
@@ -4,6 +4,7 @@ require_once __DIR__ . '/persona.php';
const CHORE_SCHEDULE_ONCE = 'once';
const CHORE_SCHEDULE_RECURRING = 'recurring';
const CHORE_NFC_HASH_ALGO = 'sha256';
/**
* @param mixed $raw
@@ -106,9 +107,60 @@ function migrateLegacyChoreRow(array $c, array $people): array {
if ($c['pending_submission'] !== null && !is_array($c['pending_submission'])) {
$c['pending_submission'] = null;
}
$c['anyone_can_complete'] = !empty($c['anyone_can_complete']);
$c['nfc'] = normalizeChoreNfcMeta($c['nfc'] ?? null);
return $c;
}
/**
* @param mixed $raw
* @return array<string, mixed>
*/
function normalizeChoreNfcMeta($raw): array {
if (!is_array($raw)) {
return [
'token_hash' => '',
'enabled' => false,
'created_at' => '',
'last_used_at' => null,
'last_used_ip' => null,
];
}
$tokenHash = isset($raw['token_hash']) ? trim((string) $raw['token_hash']) : '';
$createdAt = isset($raw['created_at']) ? trim((string) $raw['created_at']) : '';
$enabled = !empty($raw['enabled']) && $tokenHash !== '';
$lastUsedAt = isset($raw['last_used_at']) ? trim((string) $raw['last_used_at']) : '';
$lastUsedIp = isset($raw['last_used_ip']) ? trim((string) $raw['last_used_ip']) : '';
return [
'token_hash' => $tokenHash,
'enabled' => $enabled,
'created_at' => $createdAt,
'last_used_at' => $lastUsedAt !== '' ? $lastUsedAt : null,
'last_used_ip' => $lastUsedIp !== '' ? $lastUsedIp : null,
];
}
function generateOpaqueToken(int $bytes = 24): string {
$raw = random_bytes($bytes);
return rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
}
function hashOpaqueToken(string $token): string {
return hash(CHORE_NFC_HASH_ALGO, $token);
}
function validateTokenHash(string $token, string $storedHash): bool {
if ($token === '' || $storedHash === '') {
return false;
}
return hash_equals($storedHash, hashOpaqueToken($token));
}
function requestClientAddress(): ?string {
$ip = trim((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
return $ip !== '' ? $ip : null;
}
/**
* @param mixed $lists
* @return array<int, array{type: string, items: array<int, string>}>