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
354 lines
11 KiB
PHP
354 lines
11 KiB
PHP
<?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]);
|
|
}
|
|
}
|