Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 97 additions & 3 deletions prisma/seed-dummy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,37 @@ async function createTestUsers() {
});
}

console.log(`✅ Created ${userCount + 1} test users (including admin@deamap.es)`);
// Create org editor user with working password (same as admin: "123456")
await prisma.user.upsert({
where: { email: "editor@deamap.es" },
update: {},
create: {
email: "editor@deamap.es",
password_hash: "$2a$12$4K2OfmPm3siMhBLTfKrQved1KQ2kVutUh4dFwQuU1NXyWHP1eb/2C", // bcrypt hash for "123456"
name: "Editor Organización",
role: "USER",
is_active: true,
is_verified: true,
},
});

// Create verifier user with working password
await prisma.user.upsert({
where: { email: "verificador@deamap.es" },
update: {},
create: {
email: "verificador@deamap.es",
password_hash: "$2a$12$4K2OfmPm3siMhBLTfKrQved1KQ2kVutUh4dFwQuU1NXyWHP1eb/2C", // bcrypt hash for "123456"
name: "Verificador Organización",
role: "USER",
is_active: true,
is_verified: true,
},
});

console.log(
`✅ Created ${userCount + 3} test users (including admin, editor, verificador @deamap.es)`
);
}

// Generate a single AED with all related data
Expand Down Expand Up @@ -551,6 +581,69 @@ function getStatusChangeReason(from: AedStatus | null, to: AedStatus): string {
return pickRandom(reasons[key] || reasons.default);
}

// Create org memberships for test users
// Links editor and verificador to SAMUR - Protección Civil (CIVIL_PROTECTION)
async function createOrgMemberships(_orgIds: string[]) {
console.log("🔗 Creating organization memberships...");

// Find SAMUR - Protección Civil org by code
const samurCode = generateOrgCode("SAMUR - Protección Civil");
const samurOrg = await prisma.organization.findUnique({ where: { code: samurCode } });

if (!samurOrg) {
console.log(" ⚠️ SAMUR org not found, skipping memberships");
return;
}

// Assign editor@deamap.es to SAMUR with can_edit
const editor = await prisma.user.findUnique({ where: { email: "editor@deamap.es" } });
if (editor) {
await prisma.organizationMember.upsert({
where: {
organization_id_user_id: {
organization_id: samurOrg.id,
user_id: editor.id,
},
},
update: {},
create: {
organization_id: samurOrg.id,
user_id: editor.id,
can_edit: true,
can_verify: false,
can_approve: false,
can_manage_members: false,
},
});
}

// Assign verificador@deamap.es to SAMUR with can_verify + can_edit
const verificador = await prisma.user.findUnique({
where: { email: "verificador@deamap.es" },
});
if (verificador) {
await prisma.organizationMember.upsert({
where: {
organization_id_user_id: {
organization_id: samurOrg.id,
user_id: verificador.id,
},
},
update: {},
create: {
organization_id: samurOrg.id,
user_id: verificador.id,
can_edit: true,
can_verify: true,
can_approve: false,
can_manage_members: false,
},
});
}

console.log(`✅ Created org memberships for editor and verificador → ${samurOrg.name}`);
}

async function main() {
console.log("🌱 Starting dummy data seed...\n");
console.log(`📊 Configuration:`);
Expand All @@ -560,8 +653,9 @@ async function main() {

// Load reference data and create base data
await loadDistrictsReference();
await createOrganizations();
const orgIds = await createOrganizations();
await createTestUsers();
await createOrgMemberships(orgIds);

// Track sequences per district for code generation
const districtSequences = new Map<number, number>();
Expand Down Expand Up @@ -589,7 +683,7 @@ async function main() {
console.log(`\n📊 Summary:`);
console.log(` - Districts (reference): ${madridData.districts.length}`);
console.log(` - Organizations: ${madridData.organizations.length}`);
console.log(` - Test users: 20`);
console.log(` - Test users: 23 (including admin, editor, verificador)`);
console.log(` - DEAs: ${CONFIG.totalAeds}`);
console.log(` - Time: ${totalTime}s`);

Expand Down
26 changes: 22 additions & 4 deletions src/app/admin/imports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,24 @@ export default function AdminImportsPage() {
refreshInterval: 5000,
});

const canImport = user?.role === "ADMIN" || user?.permissions?.canImportAeds;

// Organizations where the user can edit (for org editors)
const editableOrgs =
user?.permissions?.organizations?.filter((o) => o.permissions.can_edit) ?? [];
const isGlobalAdmin = user?.role === "ADMIN";

useEffect(() => {
if (!authLoading && !user) {
router.push("/login?redirect=/admin/imports");
return;
}

if (!authLoading && user && user.role !== "ADMIN") {
if (!authLoading && user && !canImport) {
router.push("/");
return;
}
}, [authLoading, user, router]);
}, [authLoading, user, router, canImport]);

const handleWizardComplete = (_batchId: string) => {
refetch();
Expand Down Expand Up @@ -81,7 +88,7 @@ export default function AdminImportsPage() {
);
}

if (!user || user.role !== "ADMIN") {
if (!user || !canImport) {
return null;
}

Expand All @@ -105,6 +112,13 @@ export default function AdminImportsPage() {
<h1 className="text-3xl font-bold text-gray-900">Importaciones</h1>
<p className="mt-1 text-sm text-gray-600">
Importar DEAs masivamente desde archivos CSV
{!isGlobalAdmin && editableOrgs.length > 0 && (
<span className="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">
{editableOrgs.length === 1
? editableOrgs[0].name
: `${editableOrgs.length} organizaciones`}
</span>
)}
</p>
</div>
</div>
Expand All @@ -120,7 +134,11 @@ export default function AdminImportsPage() {
<ArrowLeft className="w-5 h-5" />
<span className="font-medium">Volver al historial</span>
</button>
<ImportWizard onComplete={handleWizardComplete} />
<ImportWizard
onComplete={handleWizardComplete}
organizations={editableOrgs}
isGlobalAdmin={isGlobalAdmin}
/>
</div>
) : (
<>
Expand Down
3 changes: 2 additions & 1 deletion src/app/api/auth/me/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export async function GET() {

// Calculate aggregated permissions from organizations
const canVerifyFromOrg = orgMemberships.some((m) => m.can_verify);
const canEditFromOrg = orgMemberships.some((m) => m.can_edit);
const canApproveFromOrg = orgMemberships.some((m) => m.can_approve);

// Check ownership of DEAs
Expand All @@ -95,7 +96,7 @@ export async function GET() {
canApprovePublications: isAdmin || canApproveFromOrg,

// Import/Export permissions
canImportAeds: isAdmin || isModerator,
canImportAeds: isAdmin || isModerator || canEditFromOrg,
canExportAeds: isAdmin || isModerator || canVerifyFromOrg,

// Owner permissions
Expand Down
32 changes: 22 additions & 10 deletions src/app/api/import/[id]/cancel/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ interface RouteParams {
*/
export async function POST(request: NextRequest, { params }: RouteParams) {
try {
// Verify authentication
// Verify authentication — requireAuth throws, never returns null
const user = await requireAuth(request);
if (!user) {
return NextResponse.json({ success: false, error: "No autenticado" }, { status: 401 });
}

const { id } = await params;

Expand All @@ -36,19 +33,34 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
// Leer metadata del job para determinar el motor
const job = await prisma.batchJob.findUnique({
where: { id },
select: { metadata: true, status: true, created_by: true },
select: { metadata: true, status: true, created_by: true, organization_id: true },
});

if (!job) {
return NextResponse.json({ success: false, error: `Job ${id} not found` }, { status: 404 });
}

// Verificar que el usuario es dueño del job o admin
// Verificar que el usuario es dueño del job, admin, o miembro de la org del job
if (job.created_by !== user.userId && user.role !== UserRole.ADMIN) {
return NextResponse.json(
{ success: false, error: "No autorizado para cancelar este job" },
{ status: 403 }
);
let hasOrgAccess = false;
if (job.organization_id) {
const membership = await prisma.organizationMember.findUnique({
where: {
organization_id_user_id: {
organization_id: job.organization_id,
user_id: user.userId,
},
},
select: { can_edit: true },
});
hasOrgAccess = !!membership?.can_edit;
}
if (!hasOrgAccess) {
return NextResponse.json(
{ success: false, error: "No autorizado para cancelar este job" },
{ status: 403 }
);
}
}

const metadata = (job.metadata || {}) as Record<string, unknown>;
Expand Down
33 changes: 23 additions & 10 deletions src/app/api/import/[id]/resume/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,8 @@ interface RouteParams {
*/
export async function POST(request: NextRequest, { params }: RouteParams) {
try {
// Verify authentication
// Verify authentication — requireAuth throws, never returns null
const user = await requireAuth(request);
if (!user) {
return NextResponse.json({ success: false, error: "No autenticado" }, { status: 401 });
}

const { id } = await params;

Expand All @@ -38,19 +35,35 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
// Leer metadata del job para determinar el motor
const job = await prisma.batchJob.findUnique({
where: { id },
select: { metadata: true, created_by: true, status: true },
select: { metadata: true, created_by: true, status: true, organization_id: true },
});

if (!job) {
return NextResponse.json({ success: false, error: `Job ${id} not found` }, { status: 404 });
}

// Verificar que el usuario es dueño del job o admin
// Verificar que el usuario es dueño del job, admin, o miembro de la org del job
if (job.created_by !== user.userId && user.role !== UserRole.ADMIN) {
return NextResponse.json(
{ success: false, error: "No autorizado para reanudar este job" },
{ status: 403 }
);
// Check if user belongs to the job's organization with can_edit
let hasOrgAccess = false;
if (job.organization_id) {
const membership = await prisma.organizationMember.findUnique({
where: {
organization_id_user_id: {
organization_id: job.organization_id,
user_id: user.userId,
},
},
select: { can_edit: true },
});
hasOrgAccess = !!membership?.can_edit;
}
if (!hasOrgAccess) {
return NextResponse.json(
{ success: false, error: "No autorizado para reanudar este job" },
{ status: 403 }
);
}
}

const metadata = (job.metadata || {}) as Record<string, unknown>;
Expand Down
Loading
Loading