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:
|
||||
|
||||
- `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`:
|
||||
- `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`
|
||||
@ -62,31 +62,36 @@ All actions are invoked through `backend/public/API.php` with `?action=<method_n
|
||||
|
||||
- Utility:
|
||||
- `health`
|
||||
- Auth / Users:
|
||||
- `userRegistration(email, password)`
|
||||
- `userLogin(email, password)`
|
||||
- `userLogout(token)`
|
||||
- `userDelete(email, password)`
|
||||
- Ingredients:
|
||||
- `ingredientList(user_id, query = "", include_global = true)`
|
||||
- `ingredientGet(user_id, 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)`
|
||||
- `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)`
|
||||
- `ingredientDelete(user_id, ingredient_id)`
|
||||
- `ingredientList(token, query = "", include_global = true)`
|
||||
- `ingredientGet(token, ingredient_id)`
|
||||
- `ingredientCreate(token, 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(token, ingredient_id)`
|
||||
- Meals:
|
||||
- `mealList(user_id, meal_type = "", with_items = false, with_totals = false)`
|
||||
- `mealGet(user_id, meal_id, with_items = true, with_totals = true)`
|
||||
- `mealCreate(user_id, name, meal_type)`
|
||||
- `mealUpdate(user_id, meal_id, name, meal_type)`
|
||||
- `mealDelete(user_id, meal_id)`
|
||||
- `mealList(token, meal_type = "", with_items = false, with_totals = false)`
|
||||
- `mealGet(token, meal_id, with_items = true, with_totals = true)`
|
||||
- `mealCreate(token, name, meal_type)`
|
||||
- `mealUpdate(token, meal_id, name, meal_type)`
|
||||
- `mealDelete(token, meal_id)`
|
||||
- Meal items:
|
||||
- `mealItemList(user_id, meal_id, with_calculated = true)`
|
||||
- `mealItemAdd(user_id, meal_id, ingredient_id, grams, position = 1)`
|
||||
- `mealItemUpdate(user_id, meal_item_id, ingredient_id, grams, position)`
|
||||
- `mealItemDelete(user_id, meal_item_id)`
|
||||
- `mealItemReorder(user_id, meal_id, ordered_item_ids)`
|
||||
- `mealItemList(token, meal_id, with_calculated = true)`
|
||||
- `mealItemAdd(token, meal_id, ingredient_id, grams, position = 1)`
|
||||
- `mealItemUpdate(token, meal_item_id, ingredient_id, grams, position)`
|
||||
- `mealItemDelete(token, meal_item_id)`
|
||||
- `mealItemReorder(token, meal_id, ordered_item_ids)`
|
||||
- Calculations:
|
||||
- `mealTotals(user_id, meal_id)`
|
||||
- `mealTotals(token, meal_id)`
|
||||
- Diary:
|
||||
- `diaryDayGet(user_id, day_date, with_totals = true)`
|
||||
- `diaryDaySetMeal(user_id, day_date, meal_type, meal_id)`
|
||||
- `diaryDayUnsetMeal(user_id, day_date, meal_type)`
|
||||
- `diaryRange(user_id, date_from, date_to)`
|
||||
- `diaryDayGet(token, day_date, with_totals = true)`
|
||||
- `diaryDaySetMeal(token, day_date, meal_type, meal_id)`
|
||||
- `diaryDayUnsetMeal(token, day_date, meal_type)`
|
||||
- `diaryRange(token, date_from, date_to)`
|
||||
|
||||
## 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.
|
||||
- If `kcal_100` is `0`, API computes kcal by formula:
|
||||
- `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
|
||||
- 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
|
||||
|
||||
@ -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.
|
||||
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.
|
||||
- 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.
|
||||
- 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)
|
||||
|
||||
- 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.
|
||||
- Connect frontend to implemented backend actions.
|
||||
- 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(
|
||||
'user_id' => $include_global ? array($user_id, null) : $user_id
|
||||
);
|
||||
@ -57,14 +149,14 @@ class API extends APIlite {
|
||||
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));
|
||||
}
|
||||
|
||||
public function ingredientCreate(
|
||||
int $user_id,
|
||||
string $token,
|
||||
string $name,
|
||||
float $protein_g_100,
|
||||
float $carbs_g_100,
|
||||
@ -73,7 +165,7 @@ class API extends APIlite {
|
||||
float $fiber_g_100 = 0,
|
||||
float $kcal_100 = 0
|
||||
): array {
|
||||
$this->assertUserExists($user_id);
|
||||
$user_id = $this->requireUserIDbyToken($token);
|
||||
$name = $this->normalizeName($name);
|
||||
$this->assertNonNegative('protein_g_100', $protein_g_100);
|
||||
$this->assertNonNegative('carbs_g_100', $carbs_g_100);
|
||||
@ -108,7 +200,7 @@ class API extends APIlite {
|
||||
}
|
||||
|
||||
public function ingredientUpdate(
|
||||
int $user_id,
|
||||
string $token,
|
||||
int $ingredient_id,
|
||||
string $name,
|
||||
float $protein_g_100,
|
||||
@ -118,7 +210,7 @@ class API extends APIlite {
|
||||
float $fiber_g_100 = 0,
|
||||
float $kcal_100 = 0
|
||||
): array {
|
||||
$this->assertUserExists($user_id);
|
||||
$user_id = $this->requireUserIDbyToken($token);
|
||||
$this->getIngredientOwned($user_id, $ingredient_id);
|
||||
$name = $this->normalizeName($name);
|
||||
$this->assertNonNegative('protein_g_100', $protein_g_100);
|
||||
@ -151,9 +243,9 @@ class API extends APIlite {
|
||||
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);
|
||||
$deleted = $this->ingredients()->ingredient($ingredient_id, null);
|
||||
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);
|
||||
$meal_type = trim($meal_type);
|
||||
if (strlen($meal_type) > 0) {
|
||||
@ -196,9 +288,9 @@ class API extends APIlite {
|
||||
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));
|
||||
$calculatedItems = null;
|
||||
if ($with_items) {
|
||||
@ -215,9 +307,9 @@ class API extends APIlite {
|
||||
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);
|
||||
$this->assertMealType($meal_type);
|
||||
$mealId = $this->meals()->mealSave(array(
|
||||
@ -229,12 +321,12 @@ class API extends APIlite {
|
||||
if ($mealId === false) {
|
||||
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);
|
||||
$name = $this->normalizeName($name);
|
||||
$this->assertMealType($meal_type);
|
||||
@ -245,12 +337,12 @@ class API extends APIlite {
|
||||
if ($updated === false) {
|
||||
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);
|
||||
$deleted = $this->meals()->meal($meal_id, null);
|
||||
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);
|
||||
return array(
|
||||
'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->assertPositive('grams', $grams);
|
||||
if ($position < 1) {
|
||||
@ -294,9 +386,9 @@ class API extends APIlite {
|
||||
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);
|
||||
$this->assertPositive('grams', $grams);
|
||||
if ($position < 1) {
|
||||
@ -315,9 +407,9 @@ class API extends APIlite {
|
||||
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);
|
||||
$deleted = $this->mealItems()->mealItem($meal_item_id, null);
|
||||
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);
|
||||
if (count($ordered_item_ids) <= 0) {
|
||||
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);
|
||||
return array(
|
||||
'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);
|
||||
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->assertMealType($meal_type);
|
||||
$meal = $this->getMealOwned($user_id, $meal_id);
|
||||
@ -415,9 +507,9 @@ class API extends APIlite {
|
||||
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->assertMealType($meal_type);
|
||||
$day = $this->getDiaryDay($user_id, $day_date);
|
||||
@ -439,9 +531,9 @@ class API extends APIlite {
|
||||
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_to);
|
||||
if ($date_from > $date_to) {
|
||||
@ -528,14 +620,17 @@ class API extends APIlite {
|
||||
return $this->optionsModel;
|
||||
}
|
||||
|
||||
private function assertUserExists(int $user_id): void
|
||||
private function requireUserIDbyToken(string $token): int
|
||||
{
|
||||
if ($user_id <= 0) {
|
||||
throw new \Exception('user_id must be > 0');
|
||||
$token = trim($token);
|
||||
if (strlen($token) <= 0) {
|
||||
throw new \Exception('token is required');
|
||||
}
|
||||
if (!$this->users()->exist($user_id)) {
|
||||
throw new \Exception('User not found');
|
||||
$user_id = $this->users()->getUserIDbyToken($token);
|
||||
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
|
||||
@ -580,6 +675,49 @@ class API extends APIlite {
|
||||
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
|
||||
{
|
||||
return round($value, 2);
|
||||
|
||||
@ -107,6 +107,12 @@ class Maintenance extends \TPsoft\DBmodel\Maintenance
|
||||
$this->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
|
||||
|
||||
@ -18,6 +18,8 @@ class Users extends \TPsoft\DBmodel\DBmodel {
|
||||
'allow_attributes' => array(
|
||||
'email' => 'varchar(255)',
|
||||
'password_hash' => 'varchar(255)',
|
||||
'token' => 'varchar(255)',
|
||||
'token_expires' => 'datetime',
|
||||
'created_at' => 'datetime'
|
||||
)
|
||||
),
|
||||
@ -76,6 +78,103 @@ class Users extends \TPsoft\DBmodel\DBmodel {
|
||||
->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