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:
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user