Skip to content

Feat/aahnik bhuahahaha | Misc Work #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Mar 13, 2025
Merged
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
55 changes: 55 additions & 0 deletions app/[orgId]/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,59 @@ export default function UsersPage({
}
};

const handleCsvUpload = async (file: File) => {
try {
const formData = new FormData();
formData.append("file", file);

const response = await fetch(`/api/orgs/${orgId}/users/csv`, {
method: "POST",
body: formData,
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(formatValidationErrors(errorData));
}

const result = await response.json();
await fetchUsers();

const failedCount = result.results?.filter(
(r: any) => r.status === "error",
).length;

if (failedCount > 0) {
console.error("Failed imports details:", {
failures: result.results.filter((r: any) => r.status === "error"),
stackTraces: result.results
.filter((r: any) => r.status === "error")
.map((r: any) => ({ email: r.email, error: r.error })),
});
}

toast({
variant: failedCount > 0 ? "destructive" : "default",
title: failedCount > 0 ? "Warning" : "Success",
description:
failedCount > 0
? `${result.message} (${failedCount} failed imports. Check console for details)`
: result.message,
});
} catch (error) {
console.error("Error uploading CSV:", {
error,
stack: error instanceof Error ? error.stack : undefined,
});
toast({
variant: "destructive",
title: "Error",
description:
error instanceof Error ? error.message : "Failed to upload CSV",
});
}
};

return (
<>
<MockAlert show={showMockAlert} />
Expand All @@ -158,6 +211,8 @@ export default function UsersPage({
setIsEditorOpen(true);
}}
onDelete={deleteUser}
allowCsvUpload={true}
onCsvUpload={handleCsvUpload}
/>

<GenericEditor
Expand Down
179 changes: 179 additions & 0 deletions app/api/orgs/[orgId]/users/csv/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { NextRequest, NextResponse } from "next/server";
import { NameIdSchema } from "@/app/api/types";
import { getOrgIdFromNameId } from "@/app/api/service";
import { parseCSV } from "@/lib/csv";
import * as userService from "../service";
import { sendEmail } from "@/lib/email";

/**
* @swagger
* /api/orgs/{orgId}/users/csv:
* post:
* summary: Bulk invite users from CSV
* description: Upload a CSV file containing user emails and roles to invite multiple users at once
* tags:
* - Organizations
* parameters:
* - in: path
* name: orgId
* required: true
* schema:
* type: string
* description: Organization name ID
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* file:
* type: string
* format: binary
* description: CSV file with columns - email,role
* responses:
* 200:
* description: Users processed successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* results:
* type: array
* items:
* type: object
* properties:
* email:
* type: string
* status:
* type: string
* enum: [success, error]
* membership:
* type: object
* error:
* type: string
* 400:
* description: Invalid request or CSV format
* 500:
* description: Server error
*/

export async function POST(
request: NextRequest,
{ params }: { params: { orgId: string } },
) {
try {
const orgId = await getOrgIdFromNameId(NameIdSchema.parse(params.orgId));

const formData = await request.formData();
const file = formData.get("file") as File | null;

if (!file) {
return NextResponse.json(
{ message: "No file uploaded" },
{ status: 400 },
);
}

// Verify file type
if (!file.name.endsWith(".csv")) {
return NextResponse.json(
{ message: "Only CSV files are allowed" },
{ status: 400 },
);
}

// Read and parse CSV
const content = await file.text();
const users = parseCSV(content);

// Process users
const results = await Promise.allSettled(
users.map(async (user) => {
try {
const membership = await userService.inviteUser(orgId, {
email: user.email,
role: user.role,
});

// Send invitation email
await sendEmail({
to: user.email,
subject: "Organization Invitation",
html: `
<h1>You've been invited!</h1>
<p>You've been invited to join an organization with the role: ${user.role}</p>
<p>Click here to accept the invitation and set up your account.</p>
`,
});

return {
email: user.email,
status: "success",
membership,
};
} catch (error) {
console.error("Error processing user:", {
email: user.email,
error,
stack: error instanceof Error ? error.stack : undefined,
});

// Handle specific error types
let errorMessage = "Unknown error";
if (error instanceof Error) {
if (error.message === "User not found") {
errorMessage = `User ${user.email} needs to register first before being invited`;
} else {
errorMessage = error.message;
}
}

return {
email: user.email,
status: "error",
error: errorMessage,
};
}
}),
);

// Prepare response with more details
const successful = results.filter(
(r) => r.status === "fulfilled" && r.value.status === "success",
).length;
const failed = results.filter(
(r) => r.status === "rejected" || r.value.status === "error",
).length;
const notFound = results.filter(
(r) =>
r.status === "fulfilled" &&
r.value.status === "error" &&
r.value.error.includes("needs to register first"),
).length;

return NextResponse.json({
message: `Processed ${users.length} users (${successful} successful, ${failed} failed, ${notFound} need to register)`,
results: results.map((r) =>
r.status === "fulfilled"
? r.value
: { status: "error", error: r.reason },
),
});
} catch (error) {
console.error("Error processing CSV:", {
error,
stack: error instanceof Error ? error.stack : undefined,
});
return NextResponse.json(
{
message:
error instanceof Error ? error.message : "Failed to process CSV",
},
{ status: 500 },
);
}
}
5 changes: 5 additions & 0 deletions app/new-org/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import OrgOnboarding from "@/components/org-onboarding";

export default function Page() {
return <OrgOnboarding />;
}
Loading
Loading