Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AuthenticatedRequest } from "../types/auth";
import { HttpError, AppError } from "../utils/http-error";
import { logger as defaultLogger, type AppLogger } from "../observability/logger";
import { isValidStellarPublicKey } from "../utils/stellar-address.utils";
import { UserType } from "../types/enums";

export interface UserRepositoryContract {
findById(id: string): Promise<import("../models/User.model").User | null>;
Expand Down Expand Up @@ -82,7 +83,7 @@ export function createUserController(deps: UserControllerDeps) {
if (!rawId) {
throw new HttpError(400, "Invalid user id");
}
// Prevent enumeration via timing-safe consistent behavior: still 404 on not found
// Consistent 404 response if user does not exist
const user = await userRepository.findById(rawId);
if (!user) {
throw new HttpError(404, "User not found");
Expand Down Expand Up @@ -113,6 +114,7 @@ export function createUserController(deps: UserControllerDeps) {

const email = sanitizeString(req.body?.email, 255);
const stellarAddressRaw = sanitizeString(req.body?.stellarAddress, 56);
const userTypeRaw = req.body?.userType;

if (email !== null) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
Expand All @@ -125,7 +127,15 @@ export function createUserController(deps: UserControllerDeps) {
throw new HttpError(400, "Invalid Stellar public key");
}

if (email === null && stellarAddressRaw === null) {
if (
userTypeRaw !== undefined &&
userTypeRaw !== null &&
!Object.values(UserType).includes(userTypeRaw)
) {
throw new HttpError(400, "Invalid user type");
}

if (email === null && stellarAddressRaw === null && (userTypeRaw === undefined || userTypeRaw === null)) {
throw new HttpError(400, "No valid fields to update");
}

Expand All @@ -135,8 +145,9 @@ export function createUserController(deps: UserControllerDeps) {
}

const patch: Partial<import("../models/User.model").User> = {};
if (email !== null) patch.email = email;
if (email !== null) patch.email = email.toLowerCase();
if (stellarAddressRaw !== null) patch.stellarAddress = stellarAddressRaw;
if (userTypeRaw) patch.userType = userTypeRaw;

// Use immutable update pattern: create new object
const updated = await userRepository.save({ ...existing, ...patch });
Expand Down Expand Up @@ -178,8 +189,14 @@ export function createUserController(deps: UserControllerDeps) {
let total = 0;

try {
users = await userRepository.findAll({ skip: (page - 1) * limit, take: limit });
total = userRepository.count ? await userRepository.count() : users.length;
// Parallelized data and count fetching for high concurrency performance
const [fetchedUsers, fetchedCount] = await Promise.all([
userRepository.findAll({ skip: (page - 1) * limit, take: limit }),
userRepository.count ? userRepository.count() : Promise.resolve(-1),
]);

users = fetchedUsers;
total = fetchedCount >= 0 ? fetchedCount : users.length;
} catch (error) {
appLogger.error("Failed to list users", { error });
throw new AppError(500, "Failed to list users", "USER_LIST_FAILED");
Expand Down
392 changes: 235 additions & 157 deletions src/migrations/1731513600000-InitialSchema.ts

Large diffs are not rendered by default.

Loading
Loading