diff --git a/backend/migrations/tenant/20260410000003_add_alias_and_name_to_users.js b/backend/migrations/tenant/20260410000003_add_alias_and_name_to_users.js new file mode 100644 index 0000000..e62053a --- /dev/null +++ b/backend/migrations/tenant/20260410000003_add_alias_and_name_to_users.js @@ -0,0 +1,30 @@ +/** + * Add 'alias' and virtual 'name' column to users table. + * + * - alias: a user-editable display name / nickname + * - name: a generated column that returns COALESCE(alias, CONCAT(firstName, ' ', lastName), email) + * so that lookup fields referencing User.name always resolve. + */ +exports.up = function (knex) { + return knex.schema.alterTable('users', (table) => { + table.string('alias', 255).nullable().after('lastName'); + table.string('name', 512).nullable().after('alias'); + }).then(() => { + // Backfill existing rows: name = alias, or firstName + lastName, or email + return knex.raw(` + UPDATE users + SET name = COALESCE( + NULLIF(alias, ''), + NULLIF(TRIM(CONCAT(COALESCE(firstName, ''), ' ', COALESCE(lastName, ''))), ''), + email + ) + `); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable('users', (table) => { + table.dropColumn('name'); + table.dropColumn('alias'); + }); +}; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4e53e58..07a1529 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -20,6 +20,8 @@ model User { password String firstName String? lastName String? + alias String? + name String? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/backend/src/models/user.model.ts b/backend/src/models/user.model.ts index ab98e1d..12d0f55 100644 --- a/backend/src/models/user.model.ts +++ b/backend/src/models/user.model.ts @@ -1,4 +1,5 @@ import { BaseModel } from './base.model'; +import { ModelOptions, QueryContext } from 'objection'; export class User extends BaseModel { static tableName = 'users'; @@ -8,6 +9,8 @@ export class User extends BaseModel { password: string; firstName?: string; lastName?: string; + alias?: string; + name?: string; isActive: boolean; createdAt: Date; updatedAt: Date; @@ -22,11 +25,37 @@ export class User extends BaseModel { password: { type: 'string' }, firstName: { type: 'string' }, lastName: { type: 'string' }, + alias: { type: 'string' }, + name: { type: 'string' }, isActive: { type: 'boolean' }, }, }; } + /** + * Compute the `name` column before insert/update so lookup fields + * referencing User.name always have a value. + */ + private computeName() { + if (this.alias) { + this.name = this.alias; + } else if (this.firstName || this.lastName) { + this.name = [this.firstName, this.lastName].filter(Boolean).join(' '); + } else if (this.email) { + this.name = this.email; + } + } + + $beforeInsert(queryContext: QueryContext) { + super.$beforeInsert(queryContext); + this.computeName(); + } + + $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) { + super.$beforeUpdate(opt, queryContext); + this.computeName(); + } + static get relationMappings() { const { UserRole } = require('./user-role.model'); const { Role } = require('./role.model'); diff --git a/backend/src/rbac/setup-users.controller.ts b/backend/src/rbac/setup-users.controller.ts index 6dbb3a4..fc66cd2 100644 --- a/backend/src/rbac/setup-users.controller.ts +++ b/backend/src/rbac/setup-users.controller.ts @@ -39,7 +39,7 @@ export class SetupUsersController { @Post() async createUser( @TenantId() tenantId: string, - @Body() data: { email: string; password: string; firstName?: string; lastName?: string }, + @Body() data: { email: string; password: string; firstName?: string; lastName?: string; alias?: string }, ) { const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); @@ -52,6 +52,7 @@ export class SetupUsersController { password: hashedPassword, firstName: data.firstName, lastName: data.lastName, + alias: data.alias, isActive: true, }); @@ -62,7 +63,7 @@ export class SetupUsersController { async updateUser( @TenantId() tenantId: string, @Param('id') id: string, - @Body() data: { email?: string; password?: string; firstName?: string; lastName?: string }, + @Body() data: { email?: string; password?: string; firstName?: string; lastName?: string; alias?: string }, ) { const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); @@ -72,6 +73,7 @@ export class SetupUsersController { if (data.email) updateData.email = data.email; if (data.firstName !== undefined) updateData.firstName = data.firstName; if (data.lastName !== undefined) updateData.lastName = data.lastName; + if (data.alias !== undefined) updateData.alias = data.alias; // Hash password if provided if (data.password) { diff --git a/frontend/pages/setup/users/[id].vue b/frontend/pages/setup/users/[id].vue index eca9a9a..21959ee 100644 --- a/frontend/pages/setup/users/[id].vue +++ b/frontend/pages/setup/users/[id].vue @@ -33,6 +33,10 @@

{{ user?.email }}

+
+ +

{{ user?.alias || 'N/A' }}

+

{{ user?.firstName || 'N/A' }}

@@ -210,6 +214,9 @@ const removeRole = async (roleId: string) => { const getUserName = (user: any) => { if (!user) return 'User'; + if (user.alias) { + return user.alias; + } if (user.firstName || user.lastName) { return [user.firstName, user.lastName].filter(Boolean).join(' '); } diff --git a/frontend/pages/setup/users/index.vue b/frontend/pages/setup/users/index.vue index bf594e4..9e0b7d1 100644 --- a/frontend/pages/setup/users/index.vue +++ b/frontend/pages/setup/users/index.vue @@ -95,6 +95,10 @@
+
+ + +
@@ -131,6 +135,10 @@ +
+ + +
@@ -187,6 +195,7 @@ const newUser = ref({ password: '', firstName: '', lastName: '', + alias: '', }); const editUser = ref({ id: '', @@ -194,6 +203,7 @@ const editUser = ref({ password: '', firstName: '', lastName: '', + alias: '', }); const userToDelete = ref(null); @@ -215,7 +225,7 @@ const createUser = async () => { await api.post('/setup/users', newUser.value); toast.success('User created successfully'); showCreateDialog.value = false; - newUser.value = { email: '', password: '', firstName: '', lastName: '' }; + newUser.value = { email: '', password: '', firstName: '', lastName: '', alias: '' }; await loadUsers(); } catch (error: any) { console.error('Failed to create user:', error); @@ -230,6 +240,7 @@ const openEditDialog = (user: any) => { password: '', firstName: user.firstName || '', lastName: user.lastName || '', + alias: user.alias || '', }; showEditDialog.value = true; }; @@ -240,6 +251,7 @@ const updateUser = async () => { email: editUser.value.email, firstName: editUser.value.firstName, lastName: editUser.value.lastName, + alias: editUser.value.alias, }; if (editUser.value.password) { payload.password = editUser.value.password; @@ -273,6 +285,9 @@ const deleteUser = async () => { }; const getUserName = (user: any) => { + if (user.alias) { + return user.alias; + } if (user.firstName || user.lastName) { return [user.firstName, user.lastName].filter(Boolean).join(' '); }