Updated to using managed types instead of

hard coded ones.
This commit is contained in:
2026-01-16 17:48:46 +02:00
parent 14e6737a1d
commit 4defe33bd3
280 changed files with 48455 additions and 62 deletions

View File

@@ -1,7 +1,20 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
import { hashPassword } from "@/lib/auth"
import { type UserRole, DEPARTMENTS, TEAMS, FIELDS } from "@/types/user"
import { type UserRole } from "@/types/user"
const hasManagedType = async (type: "field" | "department" | "team", name: string) => {
const rows = (await safeQuery("SELECT 1 FROM managed_types WHERE type = ? AND name = ? LIMIT 1", [
type,
name,
])) as any[]
if (rows.length > 0) {
return true
}
const legacyRows = (await safeQuery(`SELECT 1 FROM users WHERE ${type} = ? LIMIT 1`, [name])) as any[]
return legacyRows.length > 0
}
export async function POST(request: NextRequest) {
try {
@@ -12,17 +25,41 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "נתונים חסרים" }, { status: 400 })
}
// Validate department, team, and field
if (!FIELDS.includes(field as any)) {
return NextResponse.json({ error: "תחום לא תקין" }, { status: 400 })
// Validate department, team, and field against managed types
if (!(await hasManagedType("field", field))) {
return NextResponse.json({ error: "Invalid field." }, { status: 400 })
}
if (!DEPARTMENTS.includes(department as any)) {
return NextResponse.json({ error: "מסגרת לא תקינה" }, { status: 400 })
if (!(await hasManagedType("department", department))) {
return NextResponse.json({ error: "Invalid department." }, { status: 400 })
}
if (!TEAMS.includes(team as any)) {
return NextResponse.json({ error: "צוות לא תקין" }, { status: 400 })
if (!(await hasManagedType("team", team))) {
return NextResponse.json({ error: "Invalid team." }, { status: 400 })
}
const fieldRows = (await safeQuery("SELECT id FROM managed_types WHERE type = 'field' AND name = ?", [
field,
])) as Array<{ id: number }>
const departmentRows = (await safeQuery(
"SELECT id, parent_id AS parentId FROM managed_types WHERE type = 'department' AND name = ?",
[department],
)) as Array<{ id: number; parentId: number | null }>
const teamRows = (await safeQuery(
"SELECT id, parent_id AS parentId FROM managed_types WHERE type = 'team' AND name = ?",
[team],
)) as Array<{ id: number; parentId: number | null }>
if (fieldRows.length === 0 || departmentRows.length === 0 || teamRows.length === 0) {
return NextResponse.json({ error: "Invalid field, department, or team." }, { status: 400 })
}
if (departmentRows[0].parentId !== fieldRows[0].id) {
return NextResponse.json({ error: "Department does not belong to field." }, { status: 400 })
}
if (teamRows[0].parentId !== departmentRows[0].id) {
return NextResponse.json({ error: "Team does not belong to department." }, { status: 400 })
}
const validRoles: UserRole[] = ["user", "team_admin", "department_admin", "field_admin", "global_admin"]

View File

@@ -0,0 +1,151 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
type ManagedTypeKind = "field" | "department" | "team"
type AdminScope = {
role: string
field?: string | null
department?: string | null
team?: string | null
}
const getAdminScope = async (adminId?: string) => {
if (!adminId) {
return { ok: false, response: NextResponse.json({ error: "Missing admin id." }, { status: 400 }) }
}
const adminRows = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
adminId,
])) as AdminScope[]
if (adminRows.length === 0) {
return { ok: false, response: NextResponse.json({ error: "Admin not found." }, { status: 404 }) }
}
return { ok: true as const, admin: adminRows[0] }
}
const canManageType = async (admin: AdminScope, managedType: { type: ManagedTypeKind; name: string; parentId: number | null }) => {
if (admin.role === "global_admin") return true
if (admin.role === "field_admin") {
if (managedType.type === "field") return managedType.name === admin.field
if (managedType.type === "department") {
const fieldRows = (await safeQuery("SELECT id FROM managed_types WHERE type = 'field' AND name = ?", [
admin.field,
])) as Array<{ id: number }>
return fieldRows.length > 0 && managedType.parentId === fieldRows[0].id
}
if (managedType.type === "team") {
const departmentRows = (await safeQuery(
"SELECT parent_id AS parentId FROM managed_types WHERE id = ? AND type = 'department'",
[managedType.parentId],
)) as Array<{ parentId: number | null }>
if (departmentRows.length === 0) return false
const fieldRows = (await safeQuery("SELECT id FROM managed_types WHERE type = 'field' AND name = ?", [
admin.field,
])) as Array<{ id: number }>
return fieldRows.length > 0 && departmentRows[0].parentId === fieldRows[0].id
}
}
if (admin.role === "department_admin") {
if (managedType.type === "department") return managedType.name === admin.department
if (managedType.type === "team") {
const departmentRows = (await safeQuery(
"SELECT id FROM managed_types WHERE type = 'department' AND name = ?",
[admin.department],
)) as Array<{ id: number }>
return departmentRows.length > 0 && managedType.parentId === departmentRows[0].id
}
}
return false
}
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
try {
const { adminId } = await request.json()
const adminCheck = await getAdminScope(adminId)
if (!adminCheck.ok) {
return adminCheck.response
}
const typeRows = (await safeQuery("SELECT id, type, name, parent_id AS parentId FROM managed_types WHERE id = ?", [
params.id,
])) as Array<{ id: number; type: ManagedTypeKind; name: string; parentId: number | null }>
if (typeRows.length === 0) {
return NextResponse.json({ error: "Managed type not found." }, { status: 404 })
}
const managedType = typeRows[0]
const column = managedType.type
if (!(await canManageType(adminCheck.admin, managedType))) {
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
}
const childRows = (await safeQuery("SELECT COUNT(*) as count FROM managed_types WHERE parent_id = ?", [
managedType.id,
])) as Array<{ count: number }>
if ((childRows[0]?.count || 0) > 0) {
return NextResponse.json({ error: "Remove child items first." }, { status: 409 })
}
const usageRows = (await safeQuery(`SELECT COUNT(*) as count FROM users WHERE ${column} = ?`, [
managedType.name,
])) as Array<{ count: number }>
if ((usageRows[0]?.count || 0) > 0) {
return NextResponse.json({ error: "Value is still assigned to users." }, { status: 409 })
}
await safeQuery("DELETE FROM managed_types WHERE id = ?", [managedType.id])
return NextResponse.json({ success: true })
} catch (error) {
console.error("Managed types delete error:", error)
return NextResponse.json({ error: "Failed to delete managed type." }, { status: 500 })
}
}
export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
try {
const { adminId, name } = await request.json()
const adminCheck = await getAdminScope(adminId)
if (!adminCheck.ok) {
return adminCheck.response
}
const trimmedName = typeof name === "string" ? name.trim() : ""
if (!trimmedName) {
return NextResponse.json({ error: "Name is required." }, { status: 400 })
}
const typeRows = (await safeQuery("SELECT id, type, name, parent_id AS parentId FROM managed_types WHERE id = ?", [
params.id,
])) as Array<{ id: number; type: ManagedTypeKind; name: string; parentId: number | null }>
if (typeRows.length === 0) {
return NextResponse.json({ error: "Managed type not found." }, { status: 404 })
}
const managedType = typeRows[0]
if (!(await canManageType(adminCheck.admin, managedType))) {
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
}
await safeQuery("UPDATE managed_types SET name = ? WHERE id = ?", [trimmedName, managedType.id])
const column = managedType.type
await safeQuery(`UPDATE users SET ${column} = ? WHERE ${column} = ?`, [trimmedName, managedType.name])
return NextResponse.json({ success: true })
} catch (error: any) {
if (error?.code === "ER_DUP_ENTRY") {
return NextResponse.json({ error: "Value already exists." }, { status: 409 })
}
console.error("Managed types rename error:", error)
return NextResponse.json({ error: "Failed to rename managed type." }, { status: 500 })
}
}

View File

@@ -0,0 +1,203 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
type ManagedTypeKind = "field" | "department" | "team"
const managedTypeKinds: ManagedTypeKind[] = ["field", "department", "team"]
type AdminScope = {
role: string
field?: string | null
department?: string | null
team?: string | null
}
const getAdminScope = async (adminId?: string) => {
if (!adminId) {
return { ok: false, response: NextResponse.json({ error: "Missing admin id." }, { status: 400 }) }
}
const adminRows = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
adminId,
])) as AdminScope[]
if (adminRows.length === 0) {
return { ok: false, response: NextResponse.json({ error: "Admin not found." }, { status: 404 }) }
}
return { ok: true as const, admin: adminRows[0] }
}
export async function GET(request: NextRequest) {
try {
const adminId = request.nextUrl.searchParams.get("adminId") || undefined
const adminCheck = await getAdminScope(adminId)
if (!adminCheck.ok) {
return adminCheck.response
}
const admin = adminCheck.admin
const managedRows = (await safeQuery(
"SELECT id, type, name, parent_id AS parentId FROM managed_types ORDER BY type, name",
)) as Array<{ id: number; type: ManagedTypeKind; name: string; parentId: number | null }>
const [fieldsFromUsers, departmentsFromUsers, teamsFromUsers] = await Promise.all([
safeQuery("SELECT DISTINCT field AS name FROM users WHERE field IS NOT NULL AND field <> '' ORDER BY field"),
safeQuery(
"SELECT DISTINCT department AS name FROM users WHERE department IS NOT NULL AND department <> '' ORDER BY department",
),
safeQuery("SELECT DISTINCT team AS name FROM users WHERE team IS NOT NULL AND team <> '' ORDER BY team"),
])
const byType = {
field: managedRows.filter((row) => row.type === "field"),
department: managedRows.filter((row) => row.type === "department"),
team: managedRows.filter((row) => row.type === "team"),
}
const merge = (
managed: Array<{ id: number; name: string; parentId: number | null }>,
fromUsers: Array<{ name: string }>,
) => {
const map = new Map<
string,
{ id?: number; name: string; managed: boolean; parentId?: number | null }
>()
managed.forEach((row) => {
map.set(row.name, { id: row.id, name: row.name, managed: true, parentId: row.parentId })
})
fromUsers.forEach((row) => {
if (!map.has(row.name)) {
map.set(row.name, { name: row.name, managed: false, parentId: null })
}
})
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name))
}
let fields = merge(byType.field, fieldsFromUsers as any[])
let departments = merge(byType.department, departmentsFromUsers as any[])
let teams = merge(byType.team, teamsFromUsers as any[])
if (admin.role === "field_admin") {
fields = fields.filter((field) => field.name === admin.field)
const fieldId = fields[0]?.id
departments = fieldId ? departments.filter((dept) => dept.parentId === fieldId) : []
const departmentIds = new Set(departments.map((dept) => dept.id))
teams = teams.filter((team) => team.parentId && departmentIds.has(team.parentId))
} else if (admin.role === "department_admin") {
fields = fields.filter((field) => field.name === admin.field)
departments = departments.filter((dept) => dept.name === admin.department)
const departmentId = departments[0]?.id
teams = departmentId ? teams.filter((team) => team.parentId === departmentId) : []
} else if (admin.role === "team_admin") {
fields = fields.filter((field) => field.name === admin.field)
departments = departments.filter((dept) => dept.name === admin.department)
teams = teams.filter((team) => team.name === admin.team)
}
return NextResponse.json({ fields, departments, teams })
} catch (error) {
console.error("Managed types fetch error:", error)
return NextResponse.json({ error: "Failed to load managed types." }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const { adminId, type, name, parentId } = await request.json()
const adminCheck = await getAdminScope(adminId)
if (!adminCheck.ok) {
return adminCheck.response
}
const admin = adminCheck.admin
if (!managedTypeKinds.includes(type)) {
return NextResponse.json({ error: "Invalid type." }, { status: 400 })
}
const trimmedName = typeof name === "string" ? name.trim() : ""
if (!trimmedName) {
return NextResponse.json({ error: "Name is required." }, { status: 400 })
}
if (type === "field") {
if (admin.role !== "global_admin" && admin.role !== "field_admin") {
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
}
if (admin.role === "field_admin" && admin.field && admin.field !== trimmedName) {
return NextResponse.json({ error: "Field admins can only manage their field." }, { status: 403 })
}
await safeQuery("INSERT INTO managed_types (type, name, parent_id) VALUES (?, ?, NULL)", [type, trimmedName])
} else if (type === "department") {
if (!parentId) {
return NextResponse.json({ error: "Parent field is required." }, { status: 400 })
}
if (admin.role === "department_admin" || admin.role === "team_admin") {
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
}
if (admin.role === "field_admin") {
const fieldRows = (await safeQuery("SELECT id, name FROM managed_types WHERE id = ? AND type = 'field'", [
parentId,
])) as Array<{ id: number; name: string }>
if (fieldRows.length === 0 || fieldRows[0].name !== admin.field) {
return NextResponse.json({ error: "Field admins can only manage their field." }, { status: 403 })
}
}
await safeQuery("INSERT INTO managed_types (type, name, parent_id) VALUES (?, ?, ?)", [
type,
trimmedName,
parentId,
])
} else {
if (!parentId) {
return NextResponse.json({ error: "Parent department is required." }, { status: 400 })
}
if (admin.role === "team_admin") {
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
}
if (admin.role === "department_admin") {
const departmentRows = (await safeQuery(
"SELECT id, name FROM managed_types WHERE id = ? AND type = 'department'",
[parentId],
)) as Array<{ id: number; name: string }>
if (departmentRows.length === 0 || departmentRows[0].name !== admin.department) {
return NextResponse.json({ error: "Department admins can only manage their department." }, { status: 403 })
}
}
if (admin.role === "field_admin") {
const departmentRows = (await safeQuery(
"SELECT id, parent_id AS parentId FROM managed_types WHERE id = ? AND type = 'department'",
[parentId],
)) as Array<{ id: number; parentId: number | null }>
if (departmentRows.length === 0) {
return NextResponse.json({ error: "Department not found." }, { status: 404 })
}
const fieldRows = (await safeQuery("SELECT id, name FROM managed_types WHERE id = ?", [
departmentRows[0].parentId,
])) as Array<{ id: number; name: string }>
if (fieldRows.length === 0 || fieldRows[0].name !== admin.field) {
return NextResponse.json({ error: "Field admins can only manage their field." }, { status: 403 })
}
}
await safeQuery("INSERT INTO managed_types (type, name, parent_id) VALUES (?, ?, ?)", [
type,
trimmedName,
parentId,
])
}
return NextResponse.json({ success: true })
} catch (error: any) {
if (error?.code === "ER_DUP_ENTRY") {
return NextResponse.json({ error: "Value already exists." }, { status: 409 })
}
console.error("Managed types create error:", error)
return NextResponse.json({ error: "Failed to create managed type." }, { status: 500 })
}
}

View File

@@ -0,0 +1,122 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
const hasManagedType = async (type: "field" | "department" | "team", name: string) => {
const rows = (await safeQuery("SELECT 1 FROM managed_types WHERE type = ? AND name = ? LIMIT 1", [
type,
name,
])) as any[]
if (rows.length > 0) {
return true
}
const legacyRows = (await safeQuery(`SELECT 1 FROM users WHERE ${type} = ? LIMIT 1`, [name])) as any[]
return legacyRows.length > 0
}
export async function POST(request: NextRequest) {
try {
const { adminId, targetUserId, field, department, team } = await request.json()
if (!adminId || !targetUserId || !field || !department || !team) {
return NextResponse.json({ error: "Missing required fields." }, { status: 400 })
}
const adminRows = (await safeQuery("SELECT role, field, department, team FROM users WHERE national_id = ?", [
adminId,
])) as any[]
if (adminRows.length === 0) {
return NextResponse.json({ error: "Admin not found." }, { status: 404 })
}
const admin = adminRows[0]
const userRows = (await safeQuery("SELECT national_id, field, department, team FROM users WHERE national_id = ?", [
targetUserId,
])) as any[]
if (userRows.length === 0) {
return NextResponse.json({ error: "User not found." }, { status: 404 })
}
const targetUser = userRows[0]
const [fieldOk, departmentOk, teamOk] = await Promise.all([
hasManagedType("field", field),
hasManagedType("department", department),
hasManagedType("team", team),
])
if (!fieldOk || !departmentOk || !teamOk) {
return NextResponse.json({ error: "Invalid field, department, or team." }, { status: 400 })
}
const fieldRows = (await safeQuery("SELECT id FROM managed_types WHERE type = 'field' AND name = ?", [
field,
])) as Array<{ id: number }>
const departmentRows = (await safeQuery(
"SELECT id, parent_id AS parentId FROM managed_types WHERE type = 'department' AND name = ?",
[department],
)) as Array<{ id: number; parentId: number | null }>
const teamRows = (await safeQuery(
"SELECT id, parent_id AS parentId FROM managed_types WHERE type = 'team' AND name = ?",
[team],
)) as Array<{ id: number; parentId: number | null }>
if (fieldRows.length === 0 || departmentRows.length === 0 || teamRows.length === 0) {
return NextResponse.json({ error: "Invalid field, department, or team." }, { status: 400 })
}
if (departmentRows[0].parentId !== fieldRows[0].id) {
return NextResponse.json({ error: "Department does not belong to field." }, { status: 400 })
}
if (teamRows[0].parentId !== departmentRows[0].id) {
return NextResponse.json({ error: "Team does not belong to department." }, { status: 400 })
}
if (admin.role === "field_admin") {
if (targetUser.field !== admin.field) {
return NextResponse.json({ error: "Target user is outside your field." }, { status: 403 })
}
if (field !== admin.field) {
return NextResponse.json({ error: "Field admins can only assign within their field." }, { status: 403 })
}
} else if (admin.role === "department_admin") {
if (targetUser.department !== admin.department) {
return NextResponse.json({ error: "Target user is outside your department." }, { status: 403 })
}
if (department !== admin.department) {
return NextResponse.json({ error: "Department admins can only assign within their department." }, { status: 403 })
}
if (admin.field && field !== admin.field) {
return NextResponse.json({ error: "Department admins can only assign within their field." }, { status: 403 })
}
} else if (admin.role === "team_admin") {
if (targetUser.team !== admin.team) {
return NextResponse.json({ error: "Target user is outside your team." }, { status: 403 })
}
if (team !== admin.team) {
return NextResponse.json({ error: "Team admins can only assign within their team." }, { status: 403 })
}
if (admin.department && department !== admin.department) {
return NextResponse.json({ error: "Team admins can only assign within their department." }, { status: 403 })
}
if (admin.field && field !== admin.field) {
return NextResponse.json({ error: "Team admins can only assign within their field." }, { status: 403 })
}
} else if (admin.role !== "global_admin") {
return NextResponse.json({ error: "Insufficient permissions." }, { status: 403 })
}
await safeQuery("UPDATE users SET field = ?, department = ?, team = ? WHERE national_id = ?", [
field,
department,
team,
targetUserId,
])
return NextResponse.json({ success: true, message: "User assignment updated." })
} catch (error) {
console.error("Update user scope error:", error)
return NextResponse.json({ error: "Failed to update user." }, { status: 500 })
}
}

26
app/api/user/route.ts Normal file
View File

@@ -0,0 +1,26 @@
import { type NextRequest, NextResponse } from "next/server"
import { safeQuery } from "@/lib/database"
export async function POST(request: NextRequest) {
try {
const { nationalId } = await request.json()
if (!nationalId) {
return NextResponse.json({ error: "Missing national id." }, { status: 400 })
}
const users = (await safeQuery(
"SELECT national_id, name, is_admin, role, field, department, team, in_shelter, last_updated, must_change_password FROM users WHERE national_id = ?",
[nationalId],
)) as any[]
if (users.length === 0) {
return NextResponse.json({ error: "User not found." }, { status: 404 })
}
return NextResponse.json({ user: users[0] })
} catch (error) {
console.error("User fetch error:", error)
return NextResponse.json({ error: "Failed to load user." }, { status: 500 })
}
}