feat: connect UsersPage and SettingsPage SMTP to API + extend user roles/fields
- Migration 034: extend users table with phone, position, active columns; add new roles (receptionist, accountant, security) - Backend users.ts: accept phone/position on create, accept phone/position/active/role on update; managers can now assign all non-admin roles - types/index.ts + api.ts: add phone, position, active to User type and users create/update payload types - UsersPage: fix mapRole to handle all 5 roles, fix toStaffUser to use real phone/position/active from API, fix handleSave to pass all fields - SettingsPage: load SMTP settings from hotelSettings API on mount, save SMTP on handleSave when section === 'notify' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ const users: FastifyPluginAsync = async (fastify) => {
|
||||
const canAccess = (userSlug: string | null, role: string, slug: string) =>
|
||||
role === 'super_admin' || userSlug === slug
|
||||
|
||||
const USER_FIELDS = 'id, hotel_id, email, name, role, created_at, updated_at'
|
||||
const USER_FIELDS = 'id, hotel_id, email, name, role, phone, position, active, created_at, updated_at'
|
||||
|
||||
// ── GET /api/hotels/:slug/users ────────────────────────────────────────────
|
||||
fastify.get<SlugParam>(
|
||||
@@ -41,7 +41,7 @@ const users: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// ── POST /api/hotels/:slug/users ───────────────────────────────────────────
|
||||
fastify.post<SlugParam & { Body: {
|
||||
email: string; password: string; name: string; role?: string
|
||||
email: string; password: string; name: string; role?: string; phone?: string; position?: string
|
||||
} }>(
|
||||
'/api/hotels/:slug/users',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
@@ -56,20 +56,21 @@ const users: FastifyPluginAsync = async (fastify) => {
|
||||
const hotelId = await getHotelId(slug)
|
||||
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
||||
|
||||
const { email, password, name, role = 'housekeeper' } = request.body
|
||||
const { email, password, name, role = 'housekeeper', phone, position } = request.body
|
||||
|
||||
// Managers cannot create other managers or super_admins
|
||||
if (request.user.role === 'manager' && role !== 'housekeeper') {
|
||||
return reply.code(403).send({ error: 'Managers can only create housekeepers' })
|
||||
const managerAllowedRoles = ['housekeeper', 'receptionist', 'accountant', 'security']
|
||||
if (request.user.role === 'manager' && !managerAllowedRoles.includes(role)) {
|
||||
return reply.code(403).send({ error: 'Managers cannot create managers or super_admins' })
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 12)
|
||||
try {
|
||||
const { rows } = await db.query(
|
||||
`INSERT INTO users (hotel_id, email, password_hash, name, role)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`INSERT INTO users (hotel_id, email, password_hash, name, role, phone, position)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING ${USER_FIELDS}`,
|
||||
[hotelId, email.toLowerCase(), passwordHash, name, role],
|
||||
[hotelId, email.toLowerCase(), passwordHash, name, role, phone ?? null, position ?? null],
|
||||
)
|
||||
return reply.code(201).send(rows[0])
|
||||
} catch (err: unknown) {
|
||||
@@ -84,6 +85,7 @@ const users: FastifyPluginAsync = async (fastify) => {
|
||||
// ── PATCH /api/hotels/:slug/users/:id ─────────────────────────────────────
|
||||
fastify.patch<SlugIdParam & { Body: {
|
||||
name?: string; email?: string; role?: string; password?: string
|
||||
phone?: string; position?: string; active?: boolean
|
||||
} }>(
|
||||
'/api/hotels/:slug/users/:id',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
@@ -108,13 +110,27 @@ const users: FastifyPluginAsync = async (fastify) => {
|
||||
if (request.body.email) {
|
||||
updates.push(`email = $${idx}`); values.push(request.body.email.toLowerCase()); idx++
|
||||
}
|
||||
if (request.body.role && request.user.role === 'super_admin') {
|
||||
updates.push(`role = $${idx}`); values.push(request.body.role); idx++
|
||||
if (request.body.role) {
|
||||
// Managers can change role but not to manager/super_admin
|
||||
const managerAllowedRoles = ['housekeeper', 'receptionist', 'accountant', 'security']
|
||||
if (request.user.role === 'super_admin' ||
|
||||
(request.user.role === 'manager' && managerAllowedRoles.includes(request.body.role))) {
|
||||
updates.push(`role = $${idx}`); values.push(request.body.role); idx++
|
||||
}
|
||||
}
|
||||
if (request.body.password) {
|
||||
const hash = await bcrypt.hash(request.body.password, 12)
|
||||
updates.push(`password_hash = $${idx}`); values.push(hash); idx++
|
||||
}
|
||||
if (request.body.phone !== undefined) {
|
||||
updates.push(`phone = $${idx}`); values.push(request.body.phone || null); idx++
|
||||
}
|
||||
if (request.body.position !== undefined) {
|
||||
updates.push(`position = $${idx}`); values.push(request.body.position || null); idx++
|
||||
}
|
||||
if (request.body.active !== undefined) {
|
||||
updates.push(`active = $${idx}`); values.push(request.body.active); idx++
|
||||
}
|
||||
|
||||
if (updates.length === 0) return reply.code(400).send({ error: 'Nothing to update' })
|
||||
updates.push(`updated_at = NOW()`)
|
||||
|
||||
Reference in New Issue
Block a user