Skip to content

Commit b4107e5

Browse files
authored
Merge pull request #87 from codegasms/feat/aahnik-bhuahahaha
Feat/aahnik bhuahahaha | Misc Work
2 parents 52b7209 + 59ec112 commit b4107e5

File tree

13 files changed

+515
-111
lines changed

13 files changed

+515
-111
lines changed

app/[orgId]/users/page.tsx

+55
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,59 @@ export default function UsersPage({
145145
}
146146
};
147147

148+
const handleCsvUpload = async (file: File) => {
149+
try {
150+
const formData = new FormData();
151+
formData.append("file", file);
152+
153+
const response = await fetch(`/api/orgs/${orgId}/users/csv`, {
154+
method: "POST",
155+
body: formData,
156+
});
157+
158+
if (!response.ok) {
159+
const errorData = await response.json().catch(() => ({}));
160+
throw new Error(formatValidationErrors(errorData));
161+
}
162+
163+
const result = await response.json();
164+
await fetchUsers();
165+
166+
const failedCount = result.results?.filter(
167+
(r: any) => r.status === "error",
168+
).length;
169+
170+
if (failedCount > 0) {
171+
console.error("Failed imports details:", {
172+
failures: result.results.filter((r: any) => r.status === "error"),
173+
stackTraces: result.results
174+
.filter((r: any) => r.status === "error")
175+
.map((r: any) => ({ email: r.email, error: r.error })),
176+
});
177+
}
178+
179+
toast({
180+
variant: failedCount > 0 ? "destructive" : "default",
181+
title: failedCount > 0 ? "Warning" : "Success",
182+
description:
183+
failedCount > 0
184+
? `${result.message} (${failedCount} failed imports. Check console for details)`
185+
: result.message,
186+
});
187+
} catch (error) {
188+
console.error("Error uploading CSV:", {
189+
error,
190+
stack: error instanceof Error ? error.stack : undefined,
191+
});
192+
toast({
193+
variant: "destructive",
194+
title: "Error",
195+
description:
196+
error instanceof Error ? error.message : "Failed to upload CSV",
197+
});
198+
}
199+
};
200+
148201
return (
149202
<>
150203
<MockAlert show={showMockAlert} />
@@ -158,6 +211,8 @@ export default function UsersPage({
158211
setIsEditorOpen(true);
159212
}}
160213
onDelete={deleteUser}
214+
allowCsvUpload={true}
215+
onCsvUpload={handleCsvUpload}
161216
/>
162217

163218
<GenericEditor
+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { NameIdSchema } from "@/app/api/types";
3+
import { getOrgIdFromNameId } from "@/app/api/service";
4+
import { parseCSV } from "@/lib/csv";
5+
import * as userService from "../service";
6+
import { sendEmail } from "@/lib/email";
7+
8+
/**
9+
* @swagger
10+
* /api/orgs/{orgId}/users/csv:
11+
* post:
12+
* summary: Bulk invite users from CSV
13+
* description: Upload a CSV file containing user emails and roles to invite multiple users at once
14+
* tags:
15+
* - Organizations
16+
* parameters:
17+
* - in: path
18+
* name: orgId
19+
* required: true
20+
* schema:
21+
* type: string
22+
* description: Organization name ID
23+
* requestBody:
24+
* required: true
25+
* content:
26+
* multipart/form-data:
27+
* schema:
28+
* type: object
29+
* properties:
30+
* file:
31+
* type: string
32+
* format: binary
33+
* description: CSV file with columns - email,role
34+
* responses:
35+
* 200:
36+
* description: Users processed successfully
37+
* content:
38+
* application/json:
39+
* schema:
40+
* type: object
41+
* properties:
42+
* message:
43+
* type: string
44+
* results:
45+
* type: array
46+
* items:
47+
* type: object
48+
* properties:
49+
* email:
50+
* type: string
51+
* status:
52+
* type: string
53+
* enum: [success, error]
54+
* membership:
55+
* type: object
56+
* error:
57+
* type: string
58+
* 400:
59+
* description: Invalid request or CSV format
60+
* 500:
61+
* description: Server error
62+
*/
63+
64+
export async function POST(
65+
request: NextRequest,
66+
{ params }: { params: { orgId: string } },
67+
) {
68+
try {
69+
const orgId = await getOrgIdFromNameId(NameIdSchema.parse(params.orgId));
70+
71+
const formData = await request.formData();
72+
const file = formData.get("file") as File | null;
73+
74+
if (!file) {
75+
return NextResponse.json(
76+
{ message: "No file uploaded" },
77+
{ status: 400 },
78+
);
79+
}
80+
81+
// Verify file type
82+
if (!file.name.endsWith(".csv")) {
83+
return NextResponse.json(
84+
{ message: "Only CSV files are allowed" },
85+
{ status: 400 },
86+
);
87+
}
88+
89+
// Read and parse CSV
90+
const content = await file.text();
91+
const users = parseCSV(content);
92+
93+
// Process users
94+
const results = await Promise.allSettled(
95+
users.map(async (user) => {
96+
try {
97+
const membership = await userService.inviteUser(orgId, {
98+
email: user.email,
99+
role: user.role,
100+
});
101+
102+
// Send invitation email
103+
await sendEmail({
104+
to: user.email,
105+
subject: "Organization Invitation",
106+
html: `
107+
<h1>You've been invited!</h1>
108+
<p>You've been invited to join an organization with the role: ${user.role}</p>
109+
<p>Click here to accept the invitation and set up your account.</p>
110+
`,
111+
});
112+
113+
return {
114+
email: user.email,
115+
status: "success",
116+
membership,
117+
};
118+
} catch (error) {
119+
console.error("Error processing user:", {
120+
email: user.email,
121+
error,
122+
stack: error instanceof Error ? error.stack : undefined,
123+
});
124+
125+
// Handle specific error types
126+
let errorMessage = "Unknown error";
127+
if (error instanceof Error) {
128+
if (error.message === "User not found") {
129+
errorMessage = `User ${user.email} needs to register first before being invited`;
130+
} else {
131+
errorMessage = error.message;
132+
}
133+
}
134+
135+
return {
136+
email: user.email,
137+
status: "error",
138+
error: errorMessage,
139+
};
140+
}
141+
}),
142+
);
143+
144+
// Prepare response with more details
145+
const successful = results.filter(
146+
(r) => r.status === "fulfilled" && r.value.status === "success",
147+
).length;
148+
const failed = results.filter(
149+
(r) => r.status === "rejected" || r.value.status === "error",
150+
).length;
151+
const notFound = results.filter(
152+
(r) =>
153+
r.status === "fulfilled" &&
154+
r.value.status === "error" &&
155+
r.value.error.includes("needs to register first"),
156+
).length;
157+
158+
return NextResponse.json({
159+
message: `Processed ${users.length} users (${successful} successful, ${failed} failed, ${notFound} need to register)`,
160+
results: results.map((r) =>
161+
r.status === "fulfilled"
162+
? r.value
163+
: { status: "error", error: r.reason },
164+
),
165+
});
166+
} catch (error) {
167+
console.error("Error processing CSV:", {
168+
error,
169+
stack: error instanceof Error ? error.stack : undefined,
170+
});
171+
return NextResponse.json(
172+
{
173+
message:
174+
error instanceof Error ? error.message : "Failed to process CSV",
175+
},
176+
{ status: 500 },
177+
);
178+
}
179+
}

app/new-org/page.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import OrgOnboarding from "@/components/org-onboarding";
2+
3+
export default function Page() {
4+
return <OrgOnboarding />;
5+
}

0 commit comments

Comments
 (0)