Fix user schema to match database structure

Updated backend to use single 'name' field instead of 'first_name' and 'last_name'
to match the actual database schema where users table has a 'name' column.

Changes:
- Updated User and UserPublic interfaces to use 'name' and 'avatar_url'
- Updated user validators to use 'name' instead of first/last names
- Updated all SQL queries in user.service.ts to select u.name
- Updated search filters and sort columns
- Fixed user creation and update operations

This resolves the "column u.first_name does not exist" error.
This commit is contained in:
2026-01-26 11:45:30 -06:00
parent 6d25f5103b
commit c910ce8996
5 changed files with 50 additions and 48 deletions

View File

@@ -128,8 +128,8 @@ export async function createUser(
const user = await userService.create({ const user = await userService.create({
email: data.email, email: data.email,
password: data.password, password: data.password,
first_name: data.first_name, name: data.name,
last_name: data.last_name, avatar_url: data.avatar_url,
role_id: data.role_id, role_id: data.role_id,
is_active: data.is_active, is_active: data.is_active,
}); });

View File

@@ -32,7 +32,7 @@ router.get('/:id', userController.getUserById);
/** /**
* POST /users * POST /users
* Create a new user (admin only) * Create a new user (admin only)
* Body: { email, password, first_name, last_name, role_id, is_active? } * Body: { email, password, name, avatar_url?, role_id, is_active? }
* Response: { success, message, data: User } * Response: { success, message, data: User }
*/ */
router.post('/', requireRole('ADMIN'), validateCreateUser, userController.createUser); router.post('/', requireRole('ADMIN'), validateCreateUser, userController.createUser);
@@ -40,7 +40,7 @@ router.post('/', requireRole('ADMIN'), validateCreateUser, userController.create
/** /**
* PUT /users/:id * PUT /users/:id
* Update a user (admin can update all, self can update limited fields) * Update a user (admin can update all, self can update limited fields)
* Body: { email?, first_name?, last_name?, role_id?, is_active? } * Body: { email?, name?, avatar_url?, role_id?, is_active? }
* Response: { success, message, data: User } * Response: { success, message, data: User }
*/ */
router.put('/:id', validateUpdateUser, userController.updateUser); router.put('/:id', validateUpdateUser, userController.updateUser);

View File

@@ -61,7 +61,7 @@ export async function getAll(
if (filters?.search) { if (filters?.search) {
conditions.push( conditions.push(
`(u.first_name ILIKE $${paramIndex} OR u.last_name ILIKE $${paramIndex} OR u.email ILIKE $${paramIndex})` `(u.name ILIKE $${paramIndex} OR u.email ILIKE $${paramIndex})`
); );
params.push(`%${filters.search}%`); params.push(`%${filters.search}%`);
paramIndex++; paramIndex++;
@@ -70,7 +70,7 @@ export async function getAll(
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Validate sortBy to prevent SQL injection // Validate sortBy to prevent SQL injection
const allowedSortColumns = ['created_at', 'updated_at', 'email', 'first_name', 'last_name']; const allowedSortColumns = ['created_at', 'updated_at', 'email', 'name'];
const safeSortBy = allowedSortColumns.includes(sortBy) ? sortBy : 'created_at'; const safeSortBy = allowedSortColumns.includes(sortBy) ? sortBy : 'created_at';
const safeSortOrder = sortOrder === 'asc' ? 'ASC' : 'DESC'; const safeSortOrder = sortOrder === 'asc' ? 'ASC' : 'DESC';
@@ -88,8 +88,8 @@ export async function getAll(
SELECT SELECT
u.id, u.id,
u.email, u.email,
u.first_name, u.name,
u.last_name, u.avatar_url,
u.role_id, u.role_id,
r.name as role_name, r.name as role_name,
r.description as role_description, r.description as role_description,
@@ -109,8 +109,8 @@ export async function getAll(
const users: UserPublic[] = usersResult.rows.map((row) => ({ const users: UserPublic[] = usersResult.rows.map((row) => ({
id: row.id, id: row.id,
email: row.email, email: row.email,
first_name: row.first_name, name: row.name,
last_name: row.last_name, avatar_url: row.avatar_url,
role_id: row.role_id, role_id: row.role_id,
role: row.role_name role: row.role_name
? { ? {
@@ -154,8 +154,8 @@ export async function getById(id: number): Promise<UserPublic | null> {
SELECT SELECT
u.id, u.id,
u.email, u.email,
u.first_name, u.name,
u.last_name, u.avatar_url,
u.role_id, u.role_id,
r.name as role_name, r.name as role_name,
r.description as role_description, r.description as role_description,
@@ -179,8 +179,8 @@ export async function getById(id: number): Promise<UserPublic | null> {
return { return {
id: row.id, id: row.id,
email: row.email, email: row.email,
first_name: row.first_name, name: row.name,
last_name: row.last_name, avatar_url: row.avatar_url,
role_id: row.role_id, role_id: row.role_id,
role: row.role_name role: row.role_name
? { ? {
@@ -232,8 +232,8 @@ export async function getByEmail(email: string): Promise<User | null> {
export async function create(data: { export async function create(data: {
email: string; email: string;
password: string; password: string;
first_name: string; name: string;
last_name: string; avatar_url?: string | null;
role_id: number; role_id: number;
is_active?: boolean; is_active?: boolean;
}): Promise<UserPublic> { }): Promise<UserPublic> {
@@ -248,15 +248,15 @@ export async function create(data: {
const result = await query( const result = await query(
` `
INSERT INTO users (email, password_hash, first_name, last_name, role_id, is_active) INSERT INTO users (email, password_hash, name, avatar_url, role_id, is_active)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, email, first_name, last_name, role_id, is_active, last_login, created_at, updated_at RETURNING id, email, name, avatar_url, role_id, is_active, last_login, created_at, updated_at
`, `,
[ [
data.email.toLowerCase(), data.email.toLowerCase(),
password_hash, password_hash,
data.first_name, data.name,
data.last_name, data.avatar_url ?? null,
data.role_id, data.role_id,
data.is_active ?? true, data.is_active ?? true,
] ]
@@ -278,8 +278,8 @@ export async function update(
id: number, id: number,
data: { data: {
email?: string; email?: string;
first_name?: string; name?: string;
last_name?: string; avatar_url?: string | null;
role_id?: number; role_id?: number;
is_active?: boolean; is_active?: boolean;
} }
@@ -309,15 +309,15 @@ export async function update(
paramIndex++; paramIndex++;
} }
if (data.first_name !== undefined) { if (data.name !== undefined) {
updates.push(`first_name = $${paramIndex}`); updates.push(`name = $${paramIndex}`);
params.push(data.first_name); params.push(data.name);
paramIndex++; paramIndex++;
} }
if (data.last_name !== undefined) { if (data.avatar_url !== undefined) {
updates.push(`last_name = $${paramIndex}`); updates.push(`avatar_url = $${paramIndex}`);
params.push(data.last_name); params.push(data.avatar_url);
paramIndex++; paramIndex++;
} }

View File

@@ -26,8 +26,8 @@ export interface User {
id: number; id: number;
email: string; email: string;
password_hash: string; password_hash: string;
first_name: string; name: string;
last_name: string; avatar_url: string | null;
role_id: number; role_id: number;
role?: Role; role?: Role;
is_active: boolean; is_active: boolean;
@@ -39,8 +39,8 @@ export interface User {
export interface UserPublic { export interface UserPublic {
id: number; id: number;
email: string; email: string;
first_name: string; name: string;
last_name: string; avatar_url: string | null;
role_id: number; role_id: number;
role?: Role; role?: Role;
is_active: boolean; is_active: boolean;

View File

@@ -5,7 +5,8 @@ import { Request, Response, NextFunction } from 'express';
* Schema for creating a new user * Schema for creating a new user
* - email: required, must be valid email format * - email: required, must be valid email format
* - password: required, minimum 8 characters * - password: required, minimum 8 characters
* - name: required (first_name + last_name combined or separate) * - name: required, full name
* - avatar_url: optional, URL to avatar image
* - role_id: required, must be valid UUID * - role_id: required, must be valid UUID
* - is_active: optional, defaults to true * - is_active: optional, defaults to true
*/ */
@@ -17,14 +18,15 @@ export const createUserSchema = z.object({
password: z password: z
.string({ required_error: 'Password is required' }) .string({ required_error: 'Password is required' })
.min(8, 'Password must be at least 8 characters'), .min(8, 'Password must be at least 8 characters'),
first_name: z name: z
.string({ required_error: 'First name is required' }) .string({ required_error: 'Name is required' })
.min(1, 'First name cannot be empty') .min(1, 'Name cannot be empty')
.max(100, 'First name cannot exceed 100 characters'), .max(255, 'Name cannot exceed 255 characters'),
last_name: z avatar_url: z
.string({ required_error: 'Last name is required' }) .string()
.min(1, 'Last name cannot be empty') .url('Avatar URL must be a valid URL')
.max(100, 'Last name cannot exceed 100 characters'), .optional()
.nullable(),
role_id: z role_id: z
.number({ required_error: 'Role ID is required' }) .number({ required_error: 'Role ID is required' })
.int('Role ID must be an integer') .int('Role ID must be an integer')
@@ -43,16 +45,16 @@ export const updateUserSchema = z.object({
.email('Invalid email format') .email('Invalid email format')
.transform((val) => val.toLowerCase().trim()) .transform((val) => val.toLowerCase().trim())
.optional(), .optional(),
first_name: z name: z
.string() .string()
.min(1, 'First name cannot be empty') .min(1, 'Name cannot be empty')
.max(100, 'First name cannot exceed 100 characters') .max(255, 'Name cannot exceed 255 characters')
.optional(), .optional(),
last_name: z avatar_url: z
.string() .string()
.min(1, 'Last name cannot be empty') .url('Avatar URL must be a valid URL')
.max(100, 'Last name cannot exceed 100 characters') .optional()
.optional(), .nullable(),
role_id: z role_id: z
.number() .number()
.int('Role ID must be an integer') .int('Role ID must be an integer')