diff --git a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj
index 008de526..87b68c06 100644
--- a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj
+++ b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj
@@ -388,7 +388,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "eid-wallet_iOS/eid-wallet_iOS.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
- CURRENT_PROJECT_VERSION = 0.2.1.1;
+ CURRENT_PROJECT_VERSION = 0.2.1.2;
DEVELOPMENT_TEAM = M49C8XS835;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
@@ -436,7 +436,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "eid-wallet_iOS/eid-wallet_iOS.entitlements";
CODE_SIGN_IDENTITY = "iPhone Developer";
- CURRENT_PROJECT_VERSION = 0.2.1.1;
+ CURRENT_PROJECT_VERSION = 0.2.1.2;
DEVELOPMENT_TEAM = M49C8XS835;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
diff --git a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist
index 12652f76..7d7a310a 100644
--- a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist
+++ b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist
@@ -28,7 +28,7 @@
CFBundleVersion
- 0.2.1.1
+ 0.2.1.2
LSRequiresIPhoneOS
NSAppTransportSecurity
diff --git a/platforms/pictique-api/src/controllers/UserController.ts b/platforms/pictique-api/src/controllers/UserController.ts
index 4c98b8fa..2997c071 100644
--- a/platforms/pictique-api/src/controllers/UserController.ts
+++ b/platforms/pictique-api/src/controllers/UserController.ts
@@ -34,7 +34,7 @@ export class UserController {
search = async (req: Request, res: Response) => {
try {
- const { q } = req.query;
+ const { q, page = "1", limit = "10", verified, sort = "relevance" } = req.query;
if (!q || typeof q !== "string") {
return res
@@ -42,14 +42,99 @@ export class UserController {
.json({ error: "Search query is required" });
}
- const users = await this.userService.searchUsers(q);
- res.json(users);
+ // Validate search query length
+ if (q.trim().length < 2) {
+ return res
+ .status(400)
+ .json({ error: "Search query must be at least 2 characters long" });
+ }
+
+ // Parse and validate pagination parameters
+ const pageNum = parseInt(page as string) || 1;
+ const limitNum = Math.min(parseInt(limit as string) || 10, 50); // Cap at 50 results
+
+ if (pageNum < 1 || limitNum < 1) {
+ return res
+ .status(400)
+ .json({ error: "Invalid pagination parameters" });
+ }
+
+ // Parse verified filter
+ const verifiedOnly = verified === "true";
+
+ // Validate sort parameter
+ const validSortOptions = ["relevance", "name", "verified", "newest"];
+ const sortOption = validSortOptions.includes(sort as string) ? sort as string : "relevance";
+
+ // Get users and count in parallel
+ const [users, total] = await Promise.all([
+ this.userService.searchUsers(q, pageNum, limitNum, verifiedOnly, sortOption),
+ this.userService.getSearchUsersCount(q, verifiedOnly)
+ ]);
+
+ // Calculate pagination metadata
+ const totalPages = Math.ceil(total / limitNum);
+ const hasNextPage = pageNum < totalPages;
+ const hasPrevPage = pageNum > 1;
+
+ res.json({
+ users,
+ pagination: {
+ page: pageNum,
+ limit: limitNum,
+ total,
+ totalPages,
+ hasNextPage,
+ hasPrevPage
+ },
+ searchInfo: {
+ query: q,
+ verifiedOnly,
+ sortBy: sortOption
+ }
+ });
} catch (error) {
console.error("Error searching users:", error);
res.status(500).json({ error: "Internal server error" });
}
};
+ getSearchSuggestions = async (req: Request, res: Response) => {
+ try {
+ const { q, limit = "5" } = req.query;
+
+ if (!q || typeof q !== "string") {
+ return res
+ .status(400)
+ .json({ error: "Search query is required" });
+ }
+
+ // Parse limit parameter
+ const limitNum = Math.min(parseInt(limit as string) || 5, 20); // Cap at 20 suggestions
+
+ const suggestions = await this.userService.getSearchSuggestions(q, limitNum);
+ res.json({ suggestions });
+ } catch (error) {
+ console.error("Error getting search suggestions:", error);
+ res.status(500).json({ error: "Internal server error" });
+ }
+ };
+
+ getPopularSearches = async (req: Request, res: Response) => {
+ try {
+ const { limit = "10" } = req.query;
+
+ // Parse limit parameter
+ const limitNum = Math.min(parseInt(limit as string) || 10, 50); // Cap at 50 results
+
+ const popularSearches = await this.userService.getPopularSearches(limitNum);
+ res.json({ popularSearches });
+ } catch (error) {
+ console.error("Error getting popular searches:", error);
+ res.status(500).json({ error: "Internal server error" });
+ }
+ };
+
follow = async (req: Request, res: Response) => {
try {
const followerId = req.user?.id;
diff --git a/platforms/pictique-api/src/index.ts b/platforms/pictique-api/src/index.ts
index c0ab28b3..8d306fae 100644
--- a/platforms/pictique-api/src/index.ts
+++ b/platforms/pictique-api/src/index.ts
@@ -128,6 +128,8 @@ app.get(
// User routes
app.get("/api/users", userController.currentUser);
app.get("/api/users/search", userController.search);
+app.get("/api/users/suggestions", userController.getSearchSuggestions);
+app.get("/api/users/popular", userController.getPopularSearches);
app.post("/api/users/:id/follow", authGuard, userController.follow);
app.get("/api/users/:id", authGuard, userController.getProfileById);
app.patch("/api/users", authGuard, userController.updateProfile);
diff --git a/platforms/pictique-api/src/services/UserService.ts b/platforms/pictique-api/src/services/UserService.ts
index daab306b..8afe6bad 100644
--- a/platforms/pictique-api/src/services/UserService.ts
+++ b/platforms/pictique-api/src/services/UserService.ts
@@ -2,7 +2,7 @@ import { AppDataSource } from "../database/data-source";
import { User } from "../database/entities/User";
import { Post } from "../database/entities/Post";
import { signToken } from "../utils/jwt";
-import { Like } from "typeorm";
+import { Like, Raw } from "typeorm";
export class UserService {
userRepository = AppDataSource.getRepository(User);
@@ -38,24 +38,158 @@ export class UserService {
return await this.userRepository.findOneBy({ id });
}
- searchUsers = async (query: string) => {
- const searchQuery = query;
+ searchUsers = async (
+ query: string,
+ page: number = 1,
+ limit: number = 10,
+ verifiedOnly: boolean = false,
+ sortBy: string = "relevance"
+ ) => {
+ // Sanitize and trim the search query
+ const searchQuery = query.trim();
+
+ // Return empty array if query is too short or empty
+ if (searchQuery.length < 2) {
+ return [];
+ }
- return this.userRepository.find({
- where: [
- { name: Like(`%${searchQuery}%`) },
- { ename: Like(`%${searchQuery}%`) },
- ],
- select: {
- id: true,
- handle: true,
- name: true,
- description: true,
- avatarUrl: true,
- isVerified: true,
- },
- take: 10,
- });
+ // Validate pagination parameters
+ if (page < 1 || limit < 1 || limit > 100) {
+ return [];
+ }
+
+ // Use query builder for more complex queries
+ const queryBuilder = this.userRepository
+ .createQueryBuilder("user")
+ .select([
+ "user.id",
+ "user.handle",
+ "user.name",
+ "user.description",
+ "user.avatarUrl",
+ "user.isVerified"
+ ])
+ .where(
+ "user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.description ILIKE :query OR user.name ILIKE :fuzzyQuery OR user.ename ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
+ {
+ query: `%${searchQuery}%`,
+ fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
+ }
+ );
+
+ // Add verified filter if requested
+ if (verifiedOnly) {
+ queryBuilder.andWhere("user.isVerified = :verified", { verified: true });
+ }
+
+ // Add additional filters for better results
+ queryBuilder.andWhere("user.isArchived = :archived", { archived: false });
+
+ // Apply sorting based on sortBy parameter
+ switch (sortBy) {
+ case "name":
+ queryBuilder.orderBy("user.name", "ASC");
+ break;
+ case "verified":
+ queryBuilder.orderBy("user.isVerified", "DESC").addOrderBy("user.name", "ASC");
+ break;
+ case "newest":
+ queryBuilder.orderBy("user.createdAt", "DESC");
+ break;
+ case "relevance":
+ default:
+ // Default relevance sorting: verified first, then by name
+ queryBuilder.orderBy("user.isVerified", "DESC").addOrderBy("user.name", "ASC");
+ break;
+ }
+
+ return queryBuilder
+ .skip((page - 1) * limit)
+ .take(limit)
+ .getMany();
+ };
+
+ getSearchUsersCount = async (
+ query: string,
+ verifiedOnly: boolean = false
+ ) => {
+ // Sanitize and trim the search query
+ const searchQuery = query.trim();
+
+ // Return 0 if query is too short or empty
+ if (searchQuery.length < 2) {
+ return 0;
+ }
+
+ // Use query builder for count
+ const queryBuilder = this.userRepository
+ .createQueryBuilder("user")
+ .where(
+ "user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query OR user.description ILIKE :query OR user.name ILIKE :fuzzyQuery OR user.ename ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery",
+ {
+ query: `%${searchQuery}%`,
+ fuzzyQuery: `%${searchQuery.split('').join('%')}%` // Fuzzy search with wildcards between characters
+ }
+ );
+
+ // Add verified filter if requested
+ if (verifiedOnly) {
+ queryBuilder.andWhere("user.isVerified = :verified", { verified: true });
+ }
+
+ // Add additional filters for consistency
+ queryBuilder.andWhere("user.isArchived = :archived", { archived: false });
+
+ return queryBuilder.getCount();
+ };
+
+ getSearchSuggestions = async (query: string, limit: number = 5) => {
+ // Sanitize and trim the search query
+ const searchQuery = query.trim();
+
+ // Return empty array if query is too short
+ if (searchQuery.length < 1) {
+ return [];
+ }
+
+ // Use query builder for suggestions
+ const queryBuilder = this.userRepository
+ .createQueryBuilder("user")
+ .select([
+ "user.id",
+ "user.handle",
+ "user.name",
+ "user.ename"
+ ])
+ .where(
+ "user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query",
+ { query: `%${searchQuery}%` }
+ )
+ .andWhere("user.isArchived = :archived", { archived: false })
+ .orderBy("user.isVerified", "DESC")
+ .addOrderBy("user.name", "ASC")
+ .take(limit);
+
+ return queryBuilder.getMany();
+ };
+
+ getPopularSearches = async (limit: number = 10) => {
+ // This could be enhanced with actual search analytics in the future
+ // For now, return some sample popular searches based on verified users
+ return this.userRepository
+ .createQueryBuilder("user")
+ .select([
+ "user.id",
+ "user.handle",
+ "user.name",
+ "user.ename"
+ ])
+ .where("user.isVerified = :verified", { verified: true })
+ .andWhere("user.isArchived = :archived", { archived: false })
+ .andWhere("user.name IS NOT NULL")
+ .orderBy("user.name", "ASC")
+ .take(limit)
+ .getMany();
};
followUser = async (followerId: string, followingId: string) => {