Skip to content

Commit 11dd6b8

Browse files
authored
Merge pull request #14 from GTBitsOfGood/davidpang/user-register-mutation
2 parents c4dfa7c + 7b93712 commit 11dd6b8

File tree

2 files changed

+173
-2
lines changed

2 files changed

+173
-2
lines changed

src/app/api/users/route.test.ts

+116
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { expect, test } from "@jest/globals";
55
import { dbMock } from "@/test/dbMock";
66
import { authMock } from "@/test/authMock";
77
import { UserType } from "@prisma/client";
8+
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
89

910
test("returns 401 on unauthenticated requests", async () => {
1011
await testApiHandler({
@@ -74,3 +75,118 @@ test("returns 200 and user list on succesful request", async () => {
7475
},
7576
});
7677
});
78+
79+
test("bad form data", async () => {
80+
await testApiHandler({
81+
appHandler,
82+
async test({ fetch }) {
83+
const badFormData = new FormData();
84+
badFormData.append('inviteTokenBad', 'test_token');
85+
badFormData.append('passwordBad', 'test_password');
86+
87+
const res = await fetch({ method: "POST", body: badFormData });
88+
await expect(res.status).toEqual(400);
89+
},
90+
});
91+
});
92+
93+
const getGoodFormData = () => {
94+
const goodFormData = new FormData();
95+
goodFormData.append('inviteToken', 'test_token');
96+
goodFormData.append('password', 'test_password');
97+
return goodFormData;
98+
}
99+
100+
test("missing user invite", async () => {
101+
await testApiHandler({
102+
appHandler,
103+
async test({ fetch }) {
104+
dbMock.userInvite.findUnique.mockResolvedValue(null);
105+
106+
const res = await fetch({ method: "POST", body: getGoodFormData() });
107+
await expect(res.status).toEqual(404);
108+
},
109+
});
110+
});
111+
112+
test("expired user invite", async () => {
113+
await testApiHandler({
114+
appHandler,
115+
async test({ fetch }) {
116+
const yearAgo = new Date();
117+
yearAgo.setFullYear(new Date().getFullYear() - 1);
118+
119+
dbMock.userInvite.findUnique.mockResolvedValue({
120+
token: 'test_token',
121+
id: 0,
122+
userType: UserType.SUPER_ADMIN,
123+
email: "test_email@test.com",
124+
expiration: yearAgo,
125+
partnerDetails: null,
126+
name: ""
127+
});
128+
129+
const res = await fetch({ method: "POST", body: getGoodFormData() });
130+
await expect(res.status).toEqual(400);
131+
},
132+
});
133+
});
134+
135+
136+
test("user already exists", async () => {
137+
await testApiHandler({
138+
appHandler,
139+
async test({ fetch }) {
140+
const yearLater = new Date();
141+
yearLater.setFullYear(new Date().getFullYear() + 1);
142+
143+
dbMock.userInvite.findUnique.mockResolvedValue({
144+
token: 'test_token',
145+
id: 0,
146+
userType: UserType.SUPER_ADMIN,
147+
email: "test_email@test.com",
148+
expiration: yearLater,
149+
partnerDetails: null,
150+
name: ""
151+
});
152+
153+
dbMock.user.create.mockImplementation(() => {
154+
throw new PrismaClientKnownRequestError(
155+
'violates uniqueness constraint',
156+
{
157+
code: 'P2002',
158+
clientVersion: 'mock',
159+
meta: {},
160+
batchRequestIdx: 1
161+
}
162+
);
163+
});
164+
165+
const res = await fetch({ method: "POST", body: getGoodFormData() });
166+
await expect(res.status).toEqual(409);
167+
},
168+
});
169+
});
170+
171+
test("successful create", async () => {
172+
await testApiHandler({
173+
appHandler,
174+
async test({ fetch }) {
175+
const yearLater = new Date();
176+
yearLater.setFullYear(new Date().getFullYear() + 1);
177+
178+
dbMock.userInvite.findUnique.mockResolvedValue({
179+
token: 'test_token',
180+
id: 0,
181+
userType: UserType.SUPER_ADMIN,
182+
email: "test_email@test.com",
183+
expiration: yearLater,
184+
partnerDetails: null,
185+
name: ""
186+
});
187+
188+
const res = await fetch({ method: "POST", body: getGoodFormData() });
189+
await expect(res.status).toEqual(200);
190+
},
191+
});
192+
});

src/app/api/users/route.ts

+57-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { auth } from "@/auth";
22
import { db } from "@/db";
3-
import { authenticationError, authorizationError } from "@/util/responses";
3+
import { argumentError, conflictError, notFoundError, authenticationError, authorizationError, ok } from "@/util/responses";
44
import { UserType } from "@prisma/client";
5-
import { NextResponse } from "next/server";
5+
import { NextRequest, NextResponse } from "next/server";
6+
import { zfd } from "zod-form-data";
7+
import * as argon2 from 'argon2';
8+
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
69

710
const ALLOWED_USER_TYPES: UserType[] = [
811
UserType.ADMIN,
@@ -36,3 +39,55 @@ export async function GET() {
3639

3740
return NextResponse.json(users, { status: 200 });
3841
}
42+
43+
const schema = zfd.formData({
44+
inviteToken: zfd.text(),
45+
password: zfd.text()
46+
});
47+
48+
/**
49+
* Creates a new User record.
50+
* Parameters are passed via form data.
51+
* @param inviteToken Corresponds to token in existing UserInvite record
52+
* @param password Password for new User account
53+
* @returns 400 if bad form data or expired UserInvite
54+
* @returns 404 if UserInvite does not exist
55+
* @returns 409 if User record for corresponding UserInvite already exists
56+
* @returns 200
57+
*/
58+
export async function POST(req: NextRequest) {
59+
const parsed = schema.safeParse(await req.formData());
60+
if (!parsed.success) {
61+
return argumentError("Invalid user data");
62+
}
63+
64+
const { inviteToken, password } = parsed.data;
65+
const userInvite = await db.userInvite.findUnique({
66+
where: {
67+
token: inviteToken
68+
}
69+
});
70+
if (!userInvite) {
71+
return notFoundError("Invite does not exist");
72+
} else if (userInvite.expiration < new Date()) {
73+
return argumentError("Invite has expired");
74+
}
75+
try {
76+
await db.user.create({
77+
data: {
78+
name: userInvite.name,
79+
email: userInvite.email,
80+
passwordHash: await argon2.hash(password),
81+
type: userInvite.userType
82+
}
83+
});
84+
} catch (e) {
85+
if (e instanceof PrismaClientKnownRequestError) {
86+
if (e.code === 'P2002') {
87+
return conflictError("User already exists");
88+
}
89+
}
90+
throw e;
91+
}
92+
return ok();
93+
}

0 commit comments

Comments
 (0)