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:
353
www/api/src/modules/user/UserController.php
Normal file
353
www/api/src/modules/user/UserController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user