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:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
require_once __DIR__ . '/../includes/family_settings.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can manage NFC'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
if ($id === '') {
|
||||
sendJson(['success' => false, 'error' => 'id is required'], 400);
|
||||
}
|
||||
|
||||
$rawChores = normalizeChoresList(readJsonFile('chores.json'));
|
||||
$chores = migrateAllChores($rawChores, $people);
|
||||
$idx = findChoreIndexById($chores, $id);
|
||||
if ($idx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Chore not found'], 404);
|
||||
}
|
||||
|
||||
$choreToken = generateOpaqueToken();
|
||||
$chore = $chores[$idx];
|
||||
$nfcMeta = normalizeChoreNfcMeta($chore['nfc'] ?? null);
|
||||
$nfcMeta['token_hash'] = hashOpaqueToken($choreToken);
|
||||
$nfcMeta['enabled'] = true;
|
||||
$nfcMeta['created_at'] = gmdate('c');
|
||||
$chore['nfc'] = $nfcMeta;
|
||||
$chores[$idx] = migrateLegacyChoreRow($chore, $people);
|
||||
|
||||
$updatedPeople = $people;
|
||||
$links = [];
|
||||
$familySettings = loadFamilySettings();
|
||||
$base = familyHubAppUrl($familySettings) . '/api/chore_submit_nfc.php';
|
||||
|
||||
foreach ($updatedPeople as $pi => $person) {
|
||||
$personId = (string) ($person['id'] ?? '');
|
||||
if ($personId === '') {
|
||||
continue;
|
||||
}
|
||||
$personName = (string) ($person['name'] ?? $personId);
|
||||
$personToken = generateOpaqueToken();
|
||||
$updatedPeople[$pi]['nfc_submit_token_hash'] = hashOpaqueToken($personToken);
|
||||
$updatedPeople[$pi]['nfc_submit_token_updated_at'] = gmdate('c');
|
||||
$links[] = [
|
||||
'person_id' => $personId,
|
||||
'person_name' => $personName,
|
||||
'url' => $base . '?id=' . rawurlencode($id) . '&token=' . rawurlencode($choreToken) . '&person_token=' . rawurlencode($personToken),
|
||||
];
|
||||
}
|
||||
|
||||
if (!writeJsonFile('people.json', $updatedPeople)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save person NFC tokens'], 500);
|
||||
}
|
||||
if (!writeJsonFile('chores.json', $chores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save chore NFC token'], 500);
|
||||
}
|
||||
|
||||
sendJson([
|
||||
'success' => true,
|
||||
'enabled' => true,
|
||||
'links' => $links,
|
||||
'nfc' => $nfcMeta,
|
||||
]);
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can manage NFC'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
$enabled = !empty($body['enabled']);
|
||||
if ($id === '') {
|
||||
sendJson(['success' => false, 'error' => 'id is required'], 400);
|
||||
}
|
||||
|
||||
$rawChores = normalizeChoresList(readJsonFile('chores.json'));
|
||||
$chores = migrateAllChores($rawChores, $people);
|
||||
$idx = findChoreIndexById($chores, $id);
|
||||
if ($idx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Chore not found'], 404);
|
||||
}
|
||||
|
||||
$chore = $chores[$idx];
|
||||
$nfcMeta = normalizeChoreNfcMeta($chore['nfc'] ?? null);
|
||||
if ((string) ($nfcMeta['token_hash'] ?? '') === '' && $enabled) {
|
||||
sendJson(['success' => false, 'error' => 'Generate an NFC token first'], 400);
|
||||
}
|
||||
$nfcMeta['enabled'] = $enabled;
|
||||
$chore['nfc'] = $nfcMeta;
|
||||
$chores[$idx] = migrateLegacyChoreRow($chore, $people);
|
||||
|
||||
if (!writeJsonFile('chores.json', $chores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save chore'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'enabled' => $enabled, 'nfc' => $nfcMeta]);
|
||||
+25
-10
@@ -7,7 +7,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can review chores'], 403);
|
||||
@@ -46,13 +46,10 @@ if ($decision === 'reject') {
|
||||
sendJson(['success' => true]);
|
||||
}
|
||||
|
||||
$assignees = $row['assignee_ids'] ?? [];
|
||||
if (!is_array($assignees)) {
|
||||
$assignees = [];
|
||||
}
|
||||
$submittedBy = trim((string) ($pending['submitted_by'] ?? ''));
|
||||
$value = (float) ($row['value'] ?? 0);
|
||||
$n = count($assignees);
|
||||
$share = $n > 0 ? round($value / $n, 2) : 0.0;
|
||||
$creditedEach = 0.0;
|
||||
$creditedRecipients = 0;
|
||||
|
||||
$row['pending_submission'] = null;
|
||||
|
||||
@@ -73,15 +70,33 @@ if (!writeJsonFile('chores.json', $chores)) {
|
||||
|
||||
foreach ($people as $pi => $p) {
|
||||
$pid = (string) ($p['id'] ?? '');
|
||||
if ($pid === '' || !in_array($pid, $assignees, true)) {
|
||||
if ($pid === '') {
|
||||
continue;
|
||||
}
|
||||
if ($submittedBy !== '') {
|
||||
if ($pid !== $submittedBy) {
|
||||
continue;
|
||||
}
|
||||
$creditedEach = $value;
|
||||
} else {
|
||||
$assignees = $row['assignee_ids'] ?? [];
|
||||
if (!is_array($assignees) || !in_array($pid, $assignees, true)) {
|
||||
continue;
|
||||
}
|
||||
$n = count($assignees);
|
||||
$creditedEach = $n > 0 ? round($value / $n, 2) : 0.0;
|
||||
}
|
||||
$bal = $p['currency_balance'] ?? 0;
|
||||
$people[$pi]['currency_balance'] = (is_numeric($bal) ? (float) $bal : 0.0) + $share;
|
||||
$people[$pi]['currency_balance'] = (is_numeric($bal) ? (float) $bal : 0.0) + $creditedEach;
|
||||
$creditedRecipients++;
|
||||
}
|
||||
|
||||
if (!writeJsonFile('people.json', $people)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save people balances'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'credited_each' => $share]);
|
||||
sendJson([
|
||||
'success' => true,
|
||||
'credited_each' => $creditedEach,
|
||||
'credited_recipients' => $creditedRecipients,
|
||||
]);
|
||||
|
||||
+4
-1
@@ -7,7 +7,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
$actor = requireActivePerson($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
@@ -37,6 +37,8 @@ if ($id !== '') {
|
||||
'author_id' => (string) ($actor['id'] ?? ''),
|
||||
'pending_submission' => null,
|
||||
'status' => 'active',
|
||||
'anyone_can_complete' => false,
|
||||
'nfc' => normalizeChoreNfcMeta(null),
|
||||
];
|
||||
$idx = null;
|
||||
}
|
||||
@@ -68,6 +70,7 @@ $row['description'] = isset($body['description']) ? trim((string) $body['descrip
|
||||
$row['image'] = isset($body['image']) ? trim((string) $body['image']) : '';
|
||||
$row['lists'] = normalizeChoreLists($body['lists'] ?? []);
|
||||
$row['assignee_ids'] = $assigneeIdsClean;
|
||||
$row['anyone_can_complete'] = !empty($body['anyone_can_complete']);
|
||||
|
||||
$val = isset($body['value']) ? $body['value'] : 0;
|
||||
$row['value'] = is_numeric($val) ? max(0.0, (float) $val) : 0.0;
|
||||
|
||||
@@ -7,7 +7,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
$actor = requireActivePerson($people);
|
||||
$actorId = (string) ($actor['id'] ?? '');
|
||||
if ($actorId === '') {
|
||||
@@ -34,7 +34,7 @@ if (($row['status'] ?? '') !== 'active') {
|
||||
}
|
||||
|
||||
$assignees = $row['assignee_ids'] ?? [];
|
||||
if (!is_array($assignees) || !in_array($actorId, $assignees, true)) {
|
||||
if (empty($row['anyone_can_complete']) && (!is_array($assignees) || !in_array($actorId, $assignees, true))) {
|
||||
sendJson(['success' => false, 'error' => 'Only assignees can mark this chore complete'], 403);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/../includes/utils.php';
|
||||
require_once __DIR__ . '/../includes/persona.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
require_once __DIR__ . '/../includes/family_settings.php';
|
||||
|
||||
function renderNfcResultPage(string $title, string $message, bool $success, string $redirectUrl = '', bool $autoRedirect = false): void {
|
||||
$safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
||||
$safeMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
|
||||
$safeRedirect = htmlspecialchars($redirectUrl, ENT_QUOTES, 'UTF-8');
|
||||
$statusClass = $success ? 'success' : 'danger';
|
||||
$meta = '';
|
||||
if ($autoRedirect && $redirectUrl !== '') {
|
||||
$meta = '<meta http-equiv="refresh" content="0;url=' . $safeRedirect . '">';
|
||||
}
|
||||
echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">';
|
||||
echo '<title>' . $safeTitle . '</title>' . $meta;
|
||||
echo '<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"></head><body class="bg-light">';
|
||||
echo '<main class="container py-4"><div class="row justify-content-center"><div class="col-12 col-md-8 col-lg-6">';
|
||||
echo '<div class="card border-' . $statusClass . '">';
|
||||
echo '<div class="card-header bg-' . $statusClass . '-subtle"><strong>' . $safeTitle . '</strong></div>';
|
||||
echo '<div class="card-body"><p class="mb-3">' . $safeMessage . '</p>';
|
||||
if ($redirectUrl !== '' && !$autoRedirect) {
|
||||
echo '<a class="btn btn-primary" href="' . $safeRedirect . '">Open Family Hub</a>';
|
||||
}
|
||||
echo '</div></div></div></div></main></body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
renderNfcResultPage('Method not allowed', 'This NFC URL only supports GET requests.', false);
|
||||
}
|
||||
|
||||
$familySettings = loadFamilySettings();
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
$rawChores = normalizeChoresList(readJsonFile('chores.json'));
|
||||
$chores = migrateAllChores($rawChores, $people);
|
||||
|
||||
$id = isset($_GET['id']) ? trim((string) $_GET['id']) : '';
|
||||
$token = isset($_GET['token']) ? trim((string) $_GET['token']) : '';
|
||||
$personToken = isset($_GET['person_token']) ? trim((string) $_GET['person_token']) : '';
|
||||
$hubUrl = familyHubAppUrl($familySettings);
|
||||
$redirectUrl = $hubUrl . '/?tab=chores';
|
||||
$showConfirmation = !empty($familySettings['nfc_show_confirmation']);
|
||||
|
||||
if ($id === '' || $token === '' || $personToken === '') {
|
||||
http_response_code(400);
|
||||
renderNfcResultPage('Invalid NFC link', 'Required token information is missing from this NFC URL.', false, $redirectUrl, false);
|
||||
}
|
||||
|
||||
$personMatch = findPersonBySubmitToken($people, $personToken);
|
||||
if ($personMatch === null) {
|
||||
http_response_code(403);
|
||||
renderNfcResultPage('Invalid submitter token', 'This NFC submitter token is not valid.', false, $redirectUrl, false);
|
||||
}
|
||||
|
||||
$submitter = $personMatch['person'];
|
||||
$submitterId = (string) ($submitter['id'] ?? '');
|
||||
$submitterName = (string) ($submitter['name'] ?? 'Unknown');
|
||||
|
||||
$idx = findChoreIndexById($chores, $id);
|
||||
if ($idx === null) {
|
||||
http_response_code(404);
|
||||
renderNfcResultPage('Chore not found', 'This chore no longer exists.', false, $redirectUrl, false);
|
||||
}
|
||||
|
||||
$row = $chores[$idx];
|
||||
if (($row['status'] ?? '') !== 'active') {
|
||||
http_response_code(400);
|
||||
renderNfcResultPage('Chore inactive', 'This chore is not currently active.', false, $redirectUrl, false);
|
||||
}
|
||||
|
||||
$nfcMeta = normalizeChoreNfcMeta($row['nfc'] ?? null);
|
||||
if (empty($nfcMeta['enabled']) || !validateTokenHash($token, (string) ($nfcMeta['token_hash'] ?? ''))) {
|
||||
http_response_code(403);
|
||||
renderNfcResultPage('Invalid chore token', 'This NFC chore token is invalid or disabled.', false, $redirectUrl, false);
|
||||
}
|
||||
|
||||
if (!empty($row['pending_submission']) && is_array($row['pending_submission'])) {
|
||||
http_response_code(400);
|
||||
renderNfcResultPage('Already pending review', 'This chore is already waiting for Head of household approval.', false, $redirectUrl, false);
|
||||
}
|
||||
|
||||
$anyoneCanComplete = !empty($row['anyone_can_complete']);
|
||||
$assignees = $row['assignee_ids'] ?? [];
|
||||
if (!$anyoneCanComplete) {
|
||||
if (!is_array($assignees) || !in_array($submitterId, $assignees, true)) {
|
||||
http_response_code(403);
|
||||
renderNfcResultPage('Not assigned', 'Only assigned family members can submit this chore.', false, $redirectUrl, false);
|
||||
}
|
||||
}
|
||||
|
||||
$cooldownSeconds = (int) ($familySettings['nfc_scan_cooldown_seconds'] ?? 0);
|
||||
if ($cooldownSeconds > 0) {
|
||||
$lastUsedAt = trim((string) ($nfcMeta['last_used_at'] ?? ''));
|
||||
if ($lastUsedAt !== '') {
|
||||
$lastTs = strtotime($lastUsedAt);
|
||||
if ($lastTs !== false && (time() - $lastTs) < $cooldownSeconds) {
|
||||
http_response_code(429);
|
||||
renderNfcResultPage('Please wait', 'This tag was just scanned. Try again in a few moments.', false, $redirectUrl, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$row['pending_submission'] = [
|
||||
'submitted_at' => gmdate('c'),
|
||||
'submitted_by' => $submitterId,
|
||||
'note' => 'Submitted via NFC by ' . $submitterName,
|
||||
];
|
||||
$nfcMeta['last_used_at'] = gmdate('c');
|
||||
$nfcMeta['last_used_ip'] = requestClientAddress();
|
||||
$row['nfc'] = $nfcMeta;
|
||||
$chores[$idx] = migrateLegacyChoreRow($row, $people);
|
||||
|
||||
if (!writeJsonFile('chores.json', $chores)) {
|
||||
http_response_code(500);
|
||||
renderNfcResultPage('Save failed', 'Could not save this submission. Please try again.', false, $redirectUrl, false);
|
||||
}
|
||||
|
||||
renderNfcResultPage('Submitted for approval', 'Submitted as ' . $submitterName . '. A Head of household will approve before credit is applied.', true, $redirectUrl, !$showConfirmation);
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can manage NFC'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$action = isset($body['action']) ? trim((string) $body['action']) : '';
|
||||
|
||||
if (!in_array($action, ['disable_all_chore_nfc', 'rotate_all_person_tokens'], true)) {
|
||||
sendJson(['success' => false, 'error' => 'Invalid action'], 400);
|
||||
}
|
||||
|
||||
if ($action === 'disable_all_chore_nfc') {
|
||||
$rawChores = normalizeChoresList(readJsonFile('chores.json'));
|
||||
$chores = migrateAllChores($rawChores, $people);
|
||||
foreach ($chores as $i => $chore) {
|
||||
$meta = normalizeChoreNfcMeta($chore['nfc'] ?? null);
|
||||
$meta['enabled'] = false;
|
||||
$chore['nfc'] = $meta;
|
||||
$chores[$i] = migrateLegacyChoreRow($chore, $people);
|
||||
}
|
||||
if (!writeJsonFile('chores.json', $chores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save chores'], 500);
|
||||
}
|
||||
sendJson(['success' => true, 'action' => $action]);
|
||||
}
|
||||
|
||||
$tokens = [];
|
||||
foreach ($people as $i => $person) {
|
||||
$personId = (string) ($person['id'] ?? '');
|
||||
if ($personId === '') {
|
||||
continue;
|
||||
}
|
||||
$token = generateOpaqueToken();
|
||||
$people[$i]['nfc_submit_token_hash'] = hashOpaqueToken($token);
|
||||
$people[$i]['nfc_submit_token_updated_at'] = gmdate('c');
|
||||
$tokens[] = [
|
||||
'person_id' => $personId,
|
||||
'person_name' => (string) ($person['name'] ?? $personId),
|
||||
'person_token' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
if (!writeJsonFile('people.json', $people)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save people'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'action' => $action, 'tokens' => $tokens]);
|
||||
@@ -50,6 +50,23 @@ if (isset($body['week_starts_on'])) {
|
||||
}
|
||||
$merged['week_starts_on'] = $w;
|
||||
}
|
||||
if (array_key_exists('nfc_base_url', $body)) {
|
||||
$baseUrl = trim((string) $body['nfc_base_url']);
|
||||
if ($baseUrl !== '' && !preg_match('#^https?://#i', $baseUrl)) {
|
||||
sendJson(['success' => false, 'error' => 'nfc_base_url must start with http:// or https://'], 400);
|
||||
}
|
||||
$merged['nfc_base_url'] = rtrim($baseUrl, '/');
|
||||
}
|
||||
if (array_key_exists('nfc_show_confirmation', $body)) {
|
||||
$merged['nfc_show_confirmation'] = !empty($body['nfc_show_confirmation']);
|
||||
}
|
||||
if (array_key_exists('nfc_scan_cooldown_seconds', $body)) {
|
||||
$cooldown = (int) $body['nfc_scan_cooldown_seconds'];
|
||||
if ($cooldown < 0 || $cooldown > 600) {
|
||||
sendJson(['success' => false, 'error' => 'nfc_scan_cooldown_seconds must be 0-600'], 400);
|
||||
}
|
||||
$merged['nfc_scan_cooldown_seconds'] = $cooldown;
|
||||
}
|
||||
|
||||
$merged = normalizeLoadedFamilySettings($merged);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
if (count($people) === 0) {
|
||||
sendJson(['success' => false, 'error' => 'Use first-time setup to create the initial Head of Household'], 400);
|
||||
}
|
||||
@@ -58,6 +58,8 @@ $newPerson = [
|
||||
'birthday' => $birthday,
|
||||
'favoriteColor' => $favoriteColor,
|
||||
'currency_balance' => 0,
|
||||
'nfc_submit_token_hash' => '',
|
||||
'nfc_submit_token_updated_at' => '',
|
||||
'created_at' => gmdate('c'),
|
||||
];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
if (count($people) > 0) {
|
||||
sendJson(['success' => false, 'error' => 'People already exist; use signed-in Head of Household'], 403);
|
||||
}
|
||||
@@ -46,6 +46,8 @@ $newPerson = [
|
||||
'birthday' => $birthday,
|
||||
'favoriteColor' => $favoriteColor,
|
||||
'currency_balance' => 0,
|
||||
'nfc_submit_token_hash' => '',
|
||||
'nfc_submit_token_updated_at' => '',
|
||||
'created_at' => gmdate('c'),
|
||||
];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
assertHoHCanManagePeople($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
|
||||
@@ -6,7 +6,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
assertHoHCanManagePeople($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
|
||||
@@ -6,7 +6,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
assertHoHCanManagePeople($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can rotate NFC person tokens'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$personId = isset($body['person_id']) ? trim((string) $body['person_id']) : '';
|
||||
if ($personId === '') {
|
||||
sendJson(['success' => false, 'error' => 'person_id is required'], 400);
|
||||
}
|
||||
|
||||
$match = null;
|
||||
foreach ($people as $i => $person) {
|
||||
if (($person['id'] ?? '') === $personId) {
|
||||
$match = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($match === null) {
|
||||
sendJson(['success' => false, 'error' => 'Person not found'], 404);
|
||||
}
|
||||
|
||||
$token = generateOpaqueToken();
|
||||
$people[$match]['nfc_submit_token_hash'] = hashOpaqueToken($token);
|
||||
$people[$match]['nfc_submit_token_updated_at'] = gmdate('c');
|
||||
|
||||
if (!writeJsonFile('people.json', $people)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save person token'], 500);
|
||||
}
|
||||
|
||||
sendJson([
|
||||
'success' => true,
|
||||
'person_id' => $personId,
|
||||
'person_name' => (string) ($people[$match]['name'] ?? $personId),
|
||||
'person_token' => $token,
|
||||
]);
|
||||
Reference in New Issue
Block a user