Phase 1: Foundation - Database schema, core PHP structure, JWT auth, and User CRUD

Implemented:
- Database schema (SQLite) with tables: users, profiles, posts, swipes, matches, chats, chat_messages, events, notifications, relationships, post_likes, post_comments, refresh_tokens, media
- Core PHP structure following skeleton pattern (www/api/)
- JWT Authentication (register, login, refresh, logout)
- User profile CRUD (show, update, delete, search)
- User sub-resources (posts, friends, followers, following)
- Minimal Firebase JWT implementation for PHP without Composer
This commit is contained in:
2026-04-29 06:49:15 +02:00
parent df6cce8678
commit 937a32ad06
15 changed files with 1698 additions and 14 deletions

View File

@@ -0,0 +1,353 @@
<?php
/**
* User Controller
*/
namespace Modules\User;
use Core\Database;
use Core\Request;
use Core\Response;
use Utils\Helpers;
class UserController
{
private Database $db;
public function __construct(Database $db)
{
$this->db = $db;
}
/**
* GET /users/:id
*/
public function show(Request $request, array $params): void
{
$userId = $params['id'] ?? $request->userId ?? null;
if (!$userId) {
Response::unauthorized();
return;
}
$pdo = $this->db->getInstance();
$stmt = $pdo->prepare('
SELECT id, username, email, display_name, bio, avatar_url, photos,
gender, interested_in, age, location, interests, is_online,
last_seen, settings, created_at
FROM users WHERE id = ?
');
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user) {
Response::notFound('User not found');
return;
}
// Parse JSON fields
$user['photos'] = Helpers::parseJson($user['photos'], []);
$user['interested_in'] = Helpers::parseJson($user['interested_in'], []);
$user['location'] = Helpers::parseJson($user['location'], null);
$user['interests'] = Helpers::parseJson($user['interests'], []);
$user['settings'] = Helpers::parseJson($user['settings'], []);
$user['is_online'] = (bool) $user['is_online'];
// Check if own profile to show full data
$isOwnProfile = isset($request->userId) && $request->userId === $userId;
if (!$isOwnProfile) {
// Hide sensitive data for other users
unset($user['email']);
unset($user['settings']);
}
Response::success(['user' => $user]);
}
/**
* PATCH /users/:id
*/
public function update(Request $request, array $params): void
{
$userId = $params['id'] ?? $request->userId ?? null;
if (!$userId) {
Response::unauthorized();
return;
}
// Can only update own profile
if (!isset($request->userId) || $request->userId !== $userId) {
Response::forbidden('You can only update your own profile');
return;
}
$body = $request->getBody();
$pdo = $this->db->getInstance();
// Build update query dynamically
$allowedFields = [
'display_name', 'bio', 'avatar_url', 'photos', 'gender',
'interested_in', 'age', 'location', 'interests', 'settings'
];
$updates = [];
$values = [];
foreach ($allowedFields as $field) {
if (isset($body[$field])) {
$value = $body[$field];
// Encode arrays to JSON
if (in_array($field, ['photos', 'interested_in', 'interests'])) {
$value = Helpers::jsonEncode($value);
} elseif ($field === 'location') {
$value = Helpers::jsonEncode($value);
} elseif ($field === 'settings') {
$value = Helpers::jsonEncode($value);
} elseif ($field === 'bio') {
$value = substr($value, 0, 500); // Max 500 chars
} elseif ($field === 'age') {
$value = (int) $value;
if ($value < 18 || $value > 120) {
Response::validationError(['age' => 'Age must be between 18 and 120']);
return;
}
}
$updates[] = "{$field} = ?";
$values[] = $value;
}
}
if (empty($updates)) {
Response::validationError(['general' => 'No valid fields to update']);
return;
}
$updates[] = 'updated_at = ?';
$values[] = Helpers::timestamp();
$values[] = $userId;
$sql = 'UPDATE users SET ' . implode(', ', $updates) . ' WHERE id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute($values);
// Fetch updated user
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$userId]);
$user = $stmt->fetch();
// Parse JSON fields
$user['photos'] = Helpers::parseJson($user['photos'], []);
$user['interested_in'] = Helpers::parseJson($user['interested_in'], []);
$user['location'] = Helpers::parseJson($user['location'], null);
$user['interests'] = Helpers::parseJson($user['interests'], []);
$user['settings'] = Helpers::parseJson($user['settings'], []);
Response::success(['user' => $user], 'Profile updated successfully');
}
/**
* DELETE /users/:id
*/
public function destroy(Request $request, array $params): void
{
$userId = $params['id'] ?? $request->userId ?? null;
if (!$userId) {
Response::unauthorized();
return;
}
// Can only delete own account
if (!isset($request->userId) || $request->userId !== $userId) {
Response::forbidden('You can only delete your own account');
return;
}
$pdo = $this->db->getInstance();
// Delete user (cascade will handle related records)
$stmt = $pdo->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$userId]);
Response::success(null, 'Account deleted successfully');
}
/**
* GET /users/search
*/
public function search(Request $request): void
{
$query = $request->getQuery('q', '');
$limit = min((int) $request->getQuery('limit', 20), 100);
$offset = (int) $request->getQuery('offset', 0);
if (strlen($query) < 2) {
Response::validationError(['q' => 'Search query must be at least 2 characters']);
return;
}
$pdo = $this->db->getInstance();
$searchTerm = "%{$query}%";
$stmt = $pdo->prepare('
SELECT id, username, display_name, bio, avatar_url, age, location, interests, is_online
FROM users
WHERE (username LIKE ? OR display_name LIKE ?)
LIMIT ? OFFSET ?
');
$stmt->execute([$searchTerm, $searchTerm, $limit, $offset]);
$users = $stmt->fetchAll();
// Parse JSON fields for each user
foreach ($users as &$user) {
$user['location'] = Helpers::parseJson($user['location'], null);
$user['interests'] = Helpers::parseJson($user['interests'], []);
$user['is_online'] = (bool) $user['is_online'];
}
Response::success([
'users' => $users,
'limit' => $limit,
'offset' => $offset
]);
}
/**
* GET /users/:id/posts
*/
public function posts(Request $request, array $params): void
{
$userId = $params['id'] ?? null;
if (!$userId) {
Response::notFound('User ID required');
return;
}
$limit = min((int) $request->getQuery('limit', 20), 100);
$offset = (int) $request->getQuery('offset', 0);
$pdo = $this->db->getInstance();
$stmt = $pdo->prepare('
SELECT * FROM posts
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
');
$stmt->execute([$userId, $limit, $offset]);
$posts = $stmt->fetchAll();
// Parse JSON fields
foreach ($posts as &$post) {
$post['media_urls'] = Helpers::parseJson($post['media_urls'], []);
$post['location'] = Helpers::parseJson($post['location'], null);
}
Response::success([
'posts' => $posts,
'limit' => $limit,
'offset' => $offset
]);
}
/**
* GET /users/:id/friends
*/
public function friends(Request $request, array $params): void
{
$userId = $params['id'] ?? null;
if (!$userId) {
Response::notFound('User ID required');
return;
}
$pdo = $this->db->getInstance();
// Get mutual friends (where both users follow each other)
$stmt = $pdo->prepare('
SELECT u.id, u.username, u.display_name, u.avatar_url, u.bio, u.is_online
FROM users u
INNER JOIN relationships r1 ON u.id = r1.following_id
INNER JOIN relationships r2 ON u.id = r2.follower_id
WHERE r1.follower_id = ? AND r2.following_id = ?
AND r1.type = "friend" AND r2.type = "friend"
AND r1.status = "active" AND r2.status = "active"
');
$stmt->execute([$userId, $userId]);
$friends = $stmt->fetchAll();
foreach ($friends as &$friend) {
$friend['is_online'] = (bool) $friend['is_online'];
}
Response::success(['friends' => $friends]);
}
/**
* GET /users/:id/followers
*/
public function followers(Request $request, array $params): void
{
$userId = $params['id'] ?? null;
if (!$userId) {
Response::notFound('User ID required');
return;
}
$pdo = $this->db->getInstance();
$stmt = $pdo->prepare('
SELECT u.id, u.username, u.display_name, u.avatar_url, u.bio, u.is_online
FROM users u
INNER JOIN relationships r ON u.id = r.follower_id
WHERE r.following_id = ? AND r.status = "active"
');
$stmt->execute([$userId]);
$followers = $stmt->fetchAll();
foreach ($followers as &$follower) {
$follower['is_online'] = (bool) $follower['is_online'];
}
Response::success(['followers' => $followers]);
}
/**
* GET /users/:id/following
*/
public function following(Request $request, array $params): void
{
$userId = $params['id'] ?? null;
if (!$userId) {
Response::notFound('User ID required');
return;
}
$pdo = $this->db->getInstance();
$stmt = $pdo->prepare('
SELECT u.id, u.username, u.display_name, u.avatar_url, u.bio, u.is_online
FROM users u
INNER JOIN relationships r ON u.id = r.following_id
WHERE r.follower_id = ? AND r.status = "active"
');
$stmt->execute([$userId]);
$following = $stmt->fetchAll();
foreach ($following as &$user) {
$user['is_online'] = (bool) $user['is_online'];
}
Response::success(['following' => $following]);
}
}