added TOKEN for users,
added user*() method for API, added check TOKEN for all methods in API
This commit is contained in:
61
AGENTS.md
61
AGENTS.md
@ -41,7 +41,7 @@ It describes what the project is, what is already implemented, and what still ne
|
|||||||
Migration creates these tables:
|
Migration creates these tables:
|
||||||
|
|
||||||
- `options` (`key`, `value`) for internal settings, including DB version.
|
- `options` (`key`, `value`) for internal settings, including DB version.
|
||||||
- `users` (`user_id`, `email`, `password_hash`, `created_at`).
|
- `users` (`user_id`, `email`, `password_hash`, `token`, `token_expires`, `created_at`).
|
||||||
- `ingredients`:
|
- `ingredients`:
|
||||||
- `ingredient_id`, `user_id`, `name`
|
- `ingredient_id`, `user_id`, `name`
|
||||||
- per-100g values: `protein_g_100`, `carbs_g_100`, `sugar_g_100`, `fat_g_100`, `fiber_g_100`, `kcal_100`
|
- per-100g values: `protein_g_100`, `carbs_g_100`, `sugar_g_100`, `fat_g_100`, `fiber_g_100`, `kcal_100`
|
||||||
@ -62,31 +62,36 @@ All actions are invoked through `backend/public/API.php` with `?action=<method_n
|
|||||||
|
|
||||||
- Utility:
|
- Utility:
|
||||||
- `health`
|
- `health`
|
||||||
|
- Auth / Users:
|
||||||
|
- `userRegistration(email, password)`
|
||||||
|
- `userLogin(email, password)`
|
||||||
|
- `userLogout(token)`
|
||||||
|
- `userDelete(email, password)`
|
||||||
- Ingredients:
|
- Ingredients:
|
||||||
- `ingredientList(user_id, query = "", include_global = true)`
|
- `ingredientList(token, query = "", include_global = true)`
|
||||||
- `ingredientGet(user_id, ingredient_id)`
|
- `ingredientGet(token, ingredient_id)`
|
||||||
- `ingredientCreate(user_id, name, protein_g_100, carbs_g_100, sugar_g_100, fat_g_100, fiber_g_100 = 0, kcal_100 = 0)`
|
- `ingredientCreate(token, name, protein_g_100, carbs_g_100, sugar_g_100, fat_g_100, fiber_g_100 = 0, kcal_100 = 0)`
|
||||||
- `ingredientUpdate(user_id, ingredient_id, name, protein_g_100, carbs_g_100, sugar_g_100, fat_g_100, fiber_g_100 = 0, kcal_100 = 0)`
|
- `ingredientUpdate(token, ingredient_id, name, protein_g_100, carbs_g_100, sugar_g_100, fat_g_100, fiber_g_100 = 0, kcal_100 = 0)`
|
||||||
- `ingredientDelete(user_id, ingredient_id)`
|
- `ingredientDelete(token, ingredient_id)`
|
||||||
- Meals:
|
- Meals:
|
||||||
- `mealList(user_id, meal_type = "", with_items = false, with_totals = false)`
|
- `mealList(token, meal_type = "", with_items = false, with_totals = false)`
|
||||||
- `mealGet(user_id, meal_id, with_items = true, with_totals = true)`
|
- `mealGet(token, meal_id, with_items = true, with_totals = true)`
|
||||||
- `mealCreate(user_id, name, meal_type)`
|
- `mealCreate(token, name, meal_type)`
|
||||||
- `mealUpdate(user_id, meal_id, name, meal_type)`
|
- `mealUpdate(token, meal_id, name, meal_type)`
|
||||||
- `mealDelete(user_id, meal_id)`
|
- `mealDelete(token, meal_id)`
|
||||||
- Meal items:
|
- Meal items:
|
||||||
- `mealItemList(user_id, meal_id, with_calculated = true)`
|
- `mealItemList(token, meal_id, with_calculated = true)`
|
||||||
- `mealItemAdd(user_id, meal_id, ingredient_id, grams, position = 1)`
|
- `mealItemAdd(token, meal_id, ingredient_id, grams, position = 1)`
|
||||||
- `mealItemUpdate(user_id, meal_item_id, ingredient_id, grams, position)`
|
- `mealItemUpdate(token, meal_item_id, ingredient_id, grams, position)`
|
||||||
- `mealItemDelete(user_id, meal_item_id)`
|
- `mealItemDelete(token, meal_item_id)`
|
||||||
- `mealItemReorder(user_id, meal_id, ordered_item_ids)`
|
- `mealItemReorder(token, meal_id, ordered_item_ids)`
|
||||||
- Calculations:
|
- Calculations:
|
||||||
- `mealTotals(user_id, meal_id)`
|
- `mealTotals(token, meal_id)`
|
||||||
- Diary:
|
- Diary:
|
||||||
- `diaryDayGet(user_id, day_date, with_totals = true)`
|
- `diaryDayGet(token, day_date, with_totals = true)`
|
||||||
- `diaryDaySetMeal(user_id, day_date, meal_type, meal_id)`
|
- `diaryDaySetMeal(token, day_date, meal_type, meal_id)`
|
||||||
- `diaryDayUnsetMeal(user_id, day_date, meal_type)`
|
- `diaryDayUnsetMeal(token, day_date, meal_type)`
|
||||||
- `diaryRange(user_id, date_from, date_to)`
|
- `diaryRange(token, date_from, date_to)`
|
||||||
|
|
||||||
## Behavior and Validation Rules
|
## Behavior and Validation Rules
|
||||||
|
|
||||||
@ -96,10 +101,16 @@ All actions are invoked through `backend/public/API.php` with `?action=<method_n
|
|||||||
- Nutrition input values are validated as non-negative.
|
- Nutrition input values are validated as non-negative.
|
||||||
- If `kcal_100` is `0`, API computes kcal by formula:
|
- If `kcal_100` is `0`, API computes kcal by formula:
|
||||||
- `protein*4 + carbs*4 + fat*9`
|
- `protein*4 + carbs*4 + fat*9`
|
||||||
- Ownership checks are enforced by `user_id`:
|
- User-bound actions now require `token` and resolve `user_id` from it at method start.
|
||||||
|
- Token validation path:
|
||||||
|
- `Users::getUserIDbyToken(token)` -> `Users::verifyToken(user_id, token)`
|
||||||
|
- valid token refreshes `token_expires`
|
||||||
|
- expired token clears `token` and `token_expires` to `NULL`
|
||||||
|
- Ownership checks are enforced by resolved `user_id`:
|
||||||
- meals and meal items must belong to the user
|
- meals and meal items must belong to the user
|
||||||
- ingredients can be user-owned or global (`user_id = null`) for read/select
|
- ingredients can be user-owned or global (`user_id = null`) for read/select
|
||||||
- API currently requires an existing `users` record for almost all actions.
|
- Registration/login generate and store user token in DB.
|
||||||
|
- `userLogout(token)` invalidates session by setting `token` and `token_expires` to `NULL`.
|
||||||
|
|
||||||
## Known Pitfalls and Notes
|
## Known Pitfalls and Notes
|
||||||
|
|
||||||
@ -108,7 +119,7 @@ All actions are invoked through `backend/public/API.php` with `?action=<method_n
|
|||||||
- If someone ran migrations before FK fix, old MySQL state may still be broken.
|
- If someone ran migrations before FK fix, old MySQL state may still be broken.
|
||||||
In that case reset affected table(s) or rebuild DB from clean state.
|
In that case reset affected table(s) or rebuild DB from clean state.
|
||||||
- Some comments in `Maintenance.php` show encoding artifacts, but SQL structure is valid.
|
- Some comments in `Maintenance.php` show encoding artifacts, but SQL structure is valid.
|
||||||
- Authentication is not implemented yet; `user_id` is passed as an action parameter.
|
- Basic token auth is implemented, but token is still passed as plain API parameter.
|
||||||
- For `array` parameters (for example `ordered_item_ids`), APIlite expects JSON in request payload.
|
- For `array` parameters (for example `ordered_item_ids`), APIlite expects JSON in request payload.
|
||||||
- APIlite wraps responses with a nested `data` object. Keep this in mind on frontend parsing.
|
- APIlite wraps responses with a nested `data` object. Keep this in mind on frontend parsing.
|
||||||
|
|
||||||
@ -131,7 +142,7 @@ Frontend:
|
|||||||
|
|
||||||
## Product Behavior Target (what to build next)
|
## Product Behavior Target (what to build next)
|
||||||
|
|
||||||
- Implement auth and session handling (replace plain `user_id` input model).
|
- Harden auth (token transport/header strategy, token revoke strategy, brute-force/rate-limits).
|
||||||
- Build frontend screens for ingredients, meals, meal item editor, diary day, diary range.
|
- Build frontend screens for ingredients, meals, meal item editor, diary day, diary range.
|
||||||
- Connect frontend to implemented backend actions.
|
- Connect frontend to implemented backend actions.
|
||||||
- Add API tests for validation, ownership checks, and totals calculation consistency.
|
- Add API tests for validation, ownership checks, and totals calculation consistency.
|
||||||
|
|||||||
@ -38,9 +38,101 @@ class API extends APIlite {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ingredientList(int $user_id, string $query = '', bool $include_global = true): array
|
public function userRegistration(string $email, string $password): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$email = $this->normalizeEmail($email);
|
||||||
|
$password = $this->normalizePassword($password);
|
||||||
|
$existing = $this->users()->userBy('email', $email);
|
||||||
|
if (is_array($existing)) {
|
||||||
|
throw new \Exception('User with this email already exists');
|
||||||
|
}
|
||||||
|
$userId = $this->users()->userSave(array(
|
||||||
|
'email' => $email,
|
||||||
|
'password_hash' => $this->users()->hashString($password),
|
||||||
|
'token' => null,
|
||||||
|
'token_expires' => null,
|
||||||
|
'created_at' => '`NOW`'
|
||||||
|
));
|
||||||
|
if ($userId === false) {
|
||||||
|
throw new \Exception('Failed to register user');
|
||||||
|
}
|
||||||
|
$userId = (int) $userId;
|
||||||
|
$this->users()->generateToken($userId);
|
||||||
|
$user = $this->users()->user($userId);
|
||||||
|
if (!is_array($user)) {
|
||||||
|
throw new \Exception('Failed to load registered user');
|
||||||
|
}
|
||||||
|
return array(
|
||||||
|
'registered' => true,
|
||||||
|
'user' => $this->mapAuthUser($user)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userLogin(string $email, string $password): array
|
||||||
|
{
|
||||||
|
$email = $this->normalizeEmail($email);
|
||||||
|
$password = $this->normalizePassword($password);
|
||||||
|
if (!$this->users()->verifyUser($email, $password)) {
|
||||||
|
throw new \Exception('Invalid email or password');
|
||||||
|
}
|
||||||
|
$user = $this->users()->userBy('email', $email);
|
||||||
|
if (!is_array($user) || !isset($user['user_id'])) {
|
||||||
|
throw new \Exception('User not found');
|
||||||
|
}
|
||||||
|
$userId = (int) $user['user_id'];
|
||||||
|
$this->users()->generateToken($userId);
|
||||||
|
$user = $this->users()->user($userId);
|
||||||
|
if (!is_array($user)) {
|
||||||
|
throw new \Exception('Failed to load logged user');
|
||||||
|
}
|
||||||
|
return array(
|
||||||
|
'logged_in' => true,
|
||||||
|
'user' => $this->mapAuthUser($user)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userDelete(string $email, string $password): array
|
||||||
|
{
|
||||||
|
$email = $this->normalizeEmail($email);
|
||||||
|
$password = $this->normalizePassword($password);
|
||||||
|
if (!$this->users()->verifyUser($email, $password)) {
|
||||||
|
throw new \Exception('Invalid email or password');
|
||||||
|
}
|
||||||
|
$user = $this->users()->userBy('email', $email);
|
||||||
|
if (!is_array($user) || !isset($user['user_id'])) {
|
||||||
|
throw new \Exception('User not found');
|
||||||
|
}
|
||||||
|
$userId = (int) $user['user_id'];
|
||||||
|
$deleted = $this->users()->user($userId, null);
|
||||||
|
if ($deleted === false) {
|
||||||
|
throw new \Exception('Failed to delete user');
|
||||||
|
}
|
||||||
|
return array(
|
||||||
|
'deleted' => true,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'email' => $email
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userLogout(string $token): array
|
||||||
|
{
|
||||||
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
|
$updated = $this->users()->user($user_id, array(
|
||||||
|
'token' => null,
|
||||||
|
'token_expires' => null
|
||||||
|
));
|
||||||
|
if ($updated === false) {
|
||||||
|
throw new \Exception('Failed to logout user');
|
||||||
|
}
|
||||||
|
return array(
|
||||||
|
'logged_out' => true,
|
||||||
|
'user_id' => $user_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ingredientList(string $token, string $query = '', bool $include_global = true): array
|
||||||
|
{
|
||||||
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$search = array(
|
$search = array(
|
||||||
'user_id' => $include_global ? array($user_id, null) : $user_id
|
'user_id' => $include_global ? array($user_id, null) : $user_id
|
||||||
);
|
);
|
||||||
@ -57,14 +149,14 @@ class API extends APIlite {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ingredientGet(int $user_id, int $ingredient_id): array
|
public function ingredientGet(string $token, int $ingredient_id): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
return $this->mapIngredient($this->getIngredientAccessible($user_id, $ingredient_id));
|
return $this->mapIngredient($this->getIngredientAccessible($user_id, $ingredient_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ingredientCreate(
|
public function ingredientCreate(
|
||||||
int $user_id,
|
string $token,
|
||||||
string $name,
|
string $name,
|
||||||
float $protein_g_100,
|
float $protein_g_100,
|
||||||
float $carbs_g_100,
|
float $carbs_g_100,
|
||||||
@ -73,7 +165,7 @@ class API extends APIlite {
|
|||||||
float $fiber_g_100 = 0,
|
float $fiber_g_100 = 0,
|
||||||
float $kcal_100 = 0
|
float $kcal_100 = 0
|
||||||
): array {
|
): array {
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$name = $this->normalizeName($name);
|
$name = $this->normalizeName($name);
|
||||||
$this->assertNonNegative('protein_g_100', $protein_g_100);
|
$this->assertNonNegative('protein_g_100', $protein_g_100);
|
||||||
$this->assertNonNegative('carbs_g_100', $carbs_g_100);
|
$this->assertNonNegative('carbs_g_100', $carbs_g_100);
|
||||||
@ -108,7 +200,7 @@ class API extends APIlite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function ingredientUpdate(
|
public function ingredientUpdate(
|
||||||
int $user_id,
|
string $token,
|
||||||
int $ingredient_id,
|
int $ingredient_id,
|
||||||
string $name,
|
string $name,
|
||||||
float $protein_g_100,
|
float $protein_g_100,
|
||||||
@ -118,7 +210,7 @@ class API extends APIlite {
|
|||||||
float $fiber_g_100 = 0,
|
float $fiber_g_100 = 0,
|
||||||
float $kcal_100 = 0
|
float $kcal_100 = 0
|
||||||
): array {
|
): array {
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->getIngredientOwned($user_id, $ingredient_id);
|
$this->getIngredientOwned($user_id, $ingredient_id);
|
||||||
$name = $this->normalizeName($name);
|
$name = $this->normalizeName($name);
|
||||||
$this->assertNonNegative('protein_g_100', $protein_g_100);
|
$this->assertNonNegative('protein_g_100', $protein_g_100);
|
||||||
@ -151,9 +243,9 @@ class API extends APIlite {
|
|||||||
return $this->mapIngredient($row);
|
return $this->mapIngredient($row);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ingredientDelete(int $user_id, int $ingredient_id): array
|
public function ingredientDelete(string $token, int $ingredient_id): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->getIngredientOwned($user_id, $ingredient_id);
|
$this->getIngredientOwned($user_id, $ingredient_id);
|
||||||
$deleted = $this->ingredients()->ingredient($ingredient_id, null);
|
$deleted = $this->ingredients()->ingredient($ingredient_id, null);
|
||||||
if ($deleted === false) {
|
if ($deleted === false) {
|
||||||
@ -165,9 +257,9 @@ class API extends APIlite {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealList(int $user_id, string $meal_type = '', bool $with_items = false, bool $with_totals = false): array
|
public function mealList(string $token, string $meal_type = '', bool $with_items = false, bool $with_totals = false): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$search = array('user_id' => $user_id);
|
$search = array('user_id' => $user_id);
|
||||||
$meal_type = trim($meal_type);
|
$meal_type = trim($meal_type);
|
||||||
if (strlen($meal_type) > 0) {
|
if (strlen($meal_type) > 0) {
|
||||||
@ -196,9 +288,9 @@ class API extends APIlite {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealGet(int $user_id, int $meal_id, bool $with_items = true, bool $with_totals = true): array
|
public function mealGet(string $token, int $meal_id, bool $with_items = true, bool $with_totals = true): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$meal = $this->mapMeal($this->getMealOwned($user_id, $meal_id));
|
$meal = $this->mapMeal($this->getMealOwned($user_id, $meal_id));
|
||||||
$calculatedItems = null;
|
$calculatedItems = null;
|
||||||
if ($with_items) {
|
if ($with_items) {
|
||||||
@ -215,9 +307,9 @@ class API extends APIlite {
|
|||||||
return $meal;
|
return $meal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealCreate(int $user_id, string $name, string $meal_type): array
|
public function mealCreate(string $token, string $name, string $meal_type): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$name = $this->normalizeName($name);
|
$name = $this->normalizeName($name);
|
||||||
$this->assertMealType($meal_type);
|
$this->assertMealType($meal_type);
|
||||||
$mealId = $this->meals()->mealSave(array(
|
$mealId = $this->meals()->mealSave(array(
|
||||||
@ -229,12 +321,12 @@ class API extends APIlite {
|
|||||||
if ($mealId === false) {
|
if ($mealId === false) {
|
||||||
throw new \Exception('Failed to create meal');
|
throw new \Exception('Failed to create meal');
|
||||||
}
|
}
|
||||||
return $this->mealGet($user_id, (int) $mealId, false, false);
|
return $this->mealGet($token, (int) $mealId, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealUpdate(int $user_id, int $meal_id, string $name, string $meal_type): array
|
public function mealUpdate(string $token, int $meal_id, string $name, string $meal_type): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->getMealOwned($user_id, $meal_id);
|
$this->getMealOwned($user_id, $meal_id);
|
||||||
$name = $this->normalizeName($name);
|
$name = $this->normalizeName($name);
|
||||||
$this->assertMealType($meal_type);
|
$this->assertMealType($meal_type);
|
||||||
@ -245,12 +337,12 @@ class API extends APIlite {
|
|||||||
if ($updated === false) {
|
if ($updated === false) {
|
||||||
throw new \Exception('Failed to update meal');
|
throw new \Exception('Failed to update meal');
|
||||||
}
|
}
|
||||||
return $this->mealGet($user_id, $meal_id, false, false);
|
return $this->mealGet($token, $meal_id, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealDelete(int $user_id, int $meal_id): array
|
public function mealDelete(string $token, int $meal_id): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->getMealOwned($user_id, $meal_id);
|
$this->getMealOwned($user_id, $meal_id);
|
||||||
$deleted = $this->meals()->meal($meal_id, null);
|
$deleted = $this->meals()->meal($meal_id, null);
|
||||||
if ($deleted === false) {
|
if ($deleted === false) {
|
||||||
@ -262,9 +354,9 @@ class API extends APIlite {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealItemList(int $user_id, int $meal_id, bool $with_calculated = true): array
|
public function mealItemList(string $token, int $meal_id, bool $with_calculated = true): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->getMealOwned($user_id, $meal_id);
|
$this->getMealOwned($user_id, $meal_id);
|
||||||
return array(
|
return array(
|
||||||
'meal_id' => $meal_id,
|
'meal_id' => $meal_id,
|
||||||
@ -272,9 +364,9 @@ class API extends APIlite {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealItemAdd(int $user_id, int $meal_id, int $ingredient_id, float $grams, int $position = 1): array
|
public function mealItemAdd(string $token, int $meal_id, int $ingredient_id, float $grams, int $position = 1): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->getMealOwned($user_id, $meal_id);
|
$this->getMealOwned($user_id, $meal_id);
|
||||||
$this->assertPositive('grams', $grams);
|
$this->assertPositive('grams', $grams);
|
||||||
if ($position < 1) {
|
if ($position < 1) {
|
||||||
@ -294,9 +386,9 @@ class API extends APIlite {
|
|||||||
return $this->enrichMealItem($item);
|
return $this->enrichMealItem($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealItemUpdate(int $user_id, int $meal_item_id, int $ingredient_id, float $grams, int $position): array
|
public function mealItemUpdate(string $token, int $meal_item_id, int $ingredient_id, float $grams, int $position): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$item = $this->getMealItemOwned($user_id, $meal_item_id);
|
$item = $this->getMealItemOwned($user_id, $meal_item_id);
|
||||||
$this->assertPositive('grams', $grams);
|
$this->assertPositive('grams', $grams);
|
||||||
if ($position < 1) {
|
if ($position < 1) {
|
||||||
@ -315,9 +407,9 @@ class API extends APIlite {
|
|||||||
return $this->enrichMealItem($item);
|
return $this->enrichMealItem($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealItemDelete(int $user_id, int $meal_item_id): array
|
public function mealItemDelete(string $token, int $meal_item_id): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$item = $this->getMealItemOwned($user_id, $meal_item_id);
|
$item = $this->getMealItemOwned($user_id, $meal_item_id);
|
||||||
$deleted = $this->mealItems()->mealItem($meal_item_id, null);
|
$deleted = $this->mealItems()->mealItem($meal_item_id, null);
|
||||||
if ($deleted === false) {
|
if ($deleted === false) {
|
||||||
@ -330,9 +422,9 @@ class API extends APIlite {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealItemReorder(int $user_id, int $meal_id, array $ordered_item_ids): array
|
public function mealItemReorder(string $token, int $meal_id, array $ordered_item_ids): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->getMealOwned($user_id, $meal_id);
|
$this->getMealOwned($user_id, $meal_id);
|
||||||
if (count($ordered_item_ids) <= 0) {
|
if (count($ordered_item_ids) <= 0) {
|
||||||
throw new \Exception('ordered_item_ids cannot be empty');
|
throw new \Exception('ordered_item_ids cannot be empty');
|
||||||
@ -361,9 +453,9 @@ class API extends APIlite {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mealTotals(int $user_id, int $meal_id): array
|
public function mealTotals(string $token, int $meal_id): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->getMealOwned($user_id, $meal_id);
|
$this->getMealOwned($user_id, $meal_id);
|
||||||
return array(
|
return array(
|
||||||
'meal_id' => $meal_id,
|
'meal_id' => $meal_id,
|
||||||
@ -371,16 +463,16 @@ class API extends APIlite {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function diaryDayGet(int $user_id, string $day_date, bool $with_totals = true): array
|
public function diaryDayGet(string $token, string $day_date, bool $with_totals = true): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->assertDate($day_date);
|
$this->assertDate($day_date);
|
||||||
return $this->buildDiaryDay($user_id, $day_date, $with_totals);
|
return $this->buildDiaryDay($user_id, $day_date, $with_totals);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function diaryDaySetMeal(int $user_id, string $day_date, string $meal_type, int $meal_id): array
|
public function diaryDaySetMeal(string $token, string $day_date, string $meal_type, int $meal_id): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->assertDate($day_date);
|
$this->assertDate($day_date);
|
||||||
$this->assertMealType($meal_type);
|
$this->assertMealType($meal_type);
|
||||||
$meal = $this->getMealOwned($user_id, $meal_id);
|
$meal = $this->getMealOwned($user_id, $meal_id);
|
||||||
@ -415,9 +507,9 @@ class API extends APIlite {
|
|||||||
return $this->buildDiaryDay($user_id, $day_date, true);
|
return $this->buildDiaryDay($user_id, $day_date, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function diaryDayUnsetMeal(int $user_id, string $day_date, string $meal_type): array
|
public function diaryDayUnsetMeal(string $token, string $day_date, string $meal_type): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->assertDate($day_date);
|
$this->assertDate($day_date);
|
||||||
$this->assertMealType($meal_type);
|
$this->assertMealType($meal_type);
|
||||||
$day = $this->getDiaryDay($user_id, $day_date);
|
$day = $this->getDiaryDay($user_id, $day_date);
|
||||||
@ -439,9 +531,9 @@ class API extends APIlite {
|
|||||||
return $this->buildDiaryDay($user_id, $day_date, true);
|
return $this->buildDiaryDay($user_id, $day_date, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function diaryRange(int $user_id, string $date_from, string $date_to): array
|
public function diaryRange(string $token, string $date_from, string $date_to): array
|
||||||
{
|
{
|
||||||
$this->assertUserExists($user_id);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$this->assertDate($date_from);
|
$this->assertDate($date_from);
|
||||||
$this->assertDate($date_to);
|
$this->assertDate($date_to);
|
||||||
if ($date_from > $date_to) {
|
if ($date_from > $date_to) {
|
||||||
@ -528,14 +620,17 @@ class API extends APIlite {
|
|||||||
return $this->optionsModel;
|
return $this->optionsModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertUserExists(int $user_id): void
|
private function requireUserIDbyToken(string $token): int
|
||||||
{
|
{
|
||||||
if ($user_id <= 0) {
|
$token = trim($token);
|
||||||
throw new \Exception('user_id must be > 0');
|
if (strlen($token) <= 0) {
|
||||||
|
throw new \Exception('token is required');
|
||||||
}
|
}
|
||||||
if (!$this->users()->exist($user_id)) {
|
$user_id = $this->users()->getUserIDbyToken($token);
|
||||||
throw new \Exception('User not found');
|
if ($user_id === false || (int) $user_id <= 0) {
|
||||||
|
throw new \Exception('Invalid or expired token');
|
||||||
}
|
}
|
||||||
|
return (int) $user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertMealType(string $meal_type): void
|
private function assertMealType(string $meal_type): void
|
||||||
@ -580,6 +675,49 @@ class API extends APIlite {
|
|||||||
return $name;
|
return $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeEmail(string $email): string
|
||||||
|
{
|
||||||
|
$email = trim(strtolower($email));
|
||||||
|
if (strlen($email) <= 0) {
|
||||||
|
throw new \Exception('email is required');
|
||||||
|
}
|
||||||
|
if (strlen($email) > 255) {
|
||||||
|
throw new \Exception('email is too long');
|
||||||
|
}
|
||||||
|
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
|
||||||
|
throw new \Exception('Invalid email format');
|
||||||
|
}
|
||||||
|
return $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePassword(string $password): string
|
||||||
|
{
|
||||||
|
if (strlen($password) <= 0) {
|
||||||
|
throw new \Exception('password is required');
|
||||||
|
}
|
||||||
|
if (strlen($password) > 1024) {
|
||||||
|
throw new \Exception('password is too long');
|
||||||
|
}
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapAuthUser(array $user): array
|
||||||
|
{
|
||||||
|
$token = isset($user['token']) && strlen((string) $user['token']) > 0
|
||||||
|
? (string) $user['token']
|
||||||
|
: null;
|
||||||
|
$tokenExpires = isset($user['token_expires']) && strlen((string) $user['token_expires']) > 0
|
||||||
|
? (string) $user['token_expires']
|
||||||
|
: null;
|
||||||
|
return array(
|
||||||
|
'user_id' => (int) $user['user_id'],
|
||||||
|
'email' => (string) $user['email'],
|
||||||
|
'token' => $token,
|
||||||
|
'token_expires' => $tokenExpires,
|
||||||
|
'created_at' => isset($user['created_at']) ? (string) $user['created_at'] : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function round2(float $value): float
|
private function round2(float $value): float
|
||||||
{
|
{
|
||||||
return round($value, 2);
|
return round($value, 2);
|
||||||
|
|||||||
@ -107,6 +107,12 @@ class Maintenance extends \TPsoft\DBmodel\Maintenance
|
|||||||
$this->dbver(7);
|
$this->dbver(7);
|
||||||
$dbver = 7;
|
$dbver = 7;
|
||||||
}
|
}
|
||||||
|
if ($dbver == 7) {
|
||||||
|
$this->checkDBAdd('users', 'token', 'VARCHAR(255) DEFAULT NULL AFTER `password_hash`');
|
||||||
|
$this->checkDBAdd('users', 'token_expires', 'DATETIME DEFAULT NULL AFTER `token`');
|
||||||
|
$this->dbver(8);
|
||||||
|
$dbver = 8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function settings(string $key, ?string $value = null): string|false
|
protected function settings(string $key, ?string $value = null): string|false
|
||||||
|
|||||||
@ -18,6 +18,8 @@ class Users extends \TPsoft\DBmodel\DBmodel {
|
|||||||
'allow_attributes' => array(
|
'allow_attributes' => array(
|
||||||
'email' => 'varchar(255)',
|
'email' => 'varchar(255)',
|
||||||
'password_hash' => 'varchar(255)',
|
'password_hash' => 'varchar(255)',
|
||||||
|
'token' => 'varchar(255)',
|
||||||
|
'token_expires' => 'datetime',
|
||||||
'created_at' => 'datetime'
|
'created_at' => 'datetime'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -76,6 +78,103 @@ class Users extends \TPsoft\DBmodel\DBmodel {
|
|||||||
->toCombo($col_key, $col_value, $add_empty);
|
->toCombo($col_key, $col_value, $add_empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hashString(string $input): string {
|
||||||
|
if (strlen($input) <= 0) {
|
||||||
|
throw new \Exception('Input string is required');
|
||||||
|
}
|
||||||
|
$hash = password_hash($input, PASSWORD_DEFAULT);
|
||||||
|
if ($hash === false) {
|
||||||
|
throw new \Exception('Failed to create hash');
|
||||||
|
}
|
||||||
|
return $hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyUser(string $email, string $password): bool {
|
||||||
|
$email = trim($email);
|
||||||
|
if (strlen($email) <= 0 || strlen($password) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$user = $this->userBy('email', $email);
|
||||||
|
if (!is_array($user) || !isset($user['password_hash'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$password_hash = (string) $user['password_hash'];
|
||||||
|
if (strlen($password_hash) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return password_verify($password, $password_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateToken(int $user_id, int $ttl_seconds = 3600): string {
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
throw new \Exception('Invalid user_id');
|
||||||
|
}
|
||||||
|
if ($ttl_seconds <= 0) {
|
||||||
|
throw new \Exception('ttl_seconds must be > 0');
|
||||||
|
}
|
||||||
|
$user = $this->user($user_id);
|
||||||
|
if (!is_array($user)) {
|
||||||
|
throw new \Exception('User not found');
|
||||||
|
}
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$token_expires = date('Y-m-d H:i:s', time() + $ttl_seconds);
|
||||||
|
$updated = $this->user($user_id, array(
|
||||||
|
'token' => $token,
|
||||||
|
'token_expires' => $token_expires
|
||||||
|
));
|
||||||
|
if ($updated === false) {
|
||||||
|
throw new \Exception('Failed to save token');
|
||||||
|
}
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyToken(int $user_id, string $token): bool {
|
||||||
|
if ($user_id <= 0 || strlen($token) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$user = $this->user($user_id);
|
||||||
|
if (!is_array($user)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$stored_token = isset($user['token']) ? (string) $user['token'] : '';
|
||||||
|
$stored_expires = isset($user['token_expires']) ? (string) $user['token_expires'] : '';
|
||||||
|
if (strlen($stored_token) <= 0 || strlen($stored_expires) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$expires_ts = strtotime($stored_expires);
|
||||||
|
if ($expires_ts === false || $expires_ts <= time()) {
|
||||||
|
$this->user($user_id, array(
|
||||||
|
'token' => null,
|
||||||
|
'token_expires' => null
|
||||||
|
));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hash_equals($stored_token, $token)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$refresh_expires = date('Y-m-d H:i:s', time() + 3600);
|
||||||
|
$updated = $this->user($user_id, array(
|
||||||
|
'token_expires' => $refresh_expires
|
||||||
|
));
|
||||||
|
return $updated !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserIDbyToken(string $token): int|false {
|
||||||
|
$token = trim($token);
|
||||||
|
if (strlen($token) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$user = $this->userBy('token', $token);
|
||||||
|
if (!is_array($user) || !isset($user['user_id'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$user_id = (int) $user['user_id'];
|
||||||
|
if (!$this->verifyToken($user_id, $token)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $user_id;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
Reference in New Issue
Block a user