Skip to content

Commit d291017

Browse files
authored
Adding user-data test and tightening security (#240)
- Move user_data table to its own file - Add a `CanSeePersonalData` auth rule. - Validate mutation works for creation and editing. - Adds zod to some inserts/updates
1 parent 7941a2e commit d291017

10 files changed

Lines changed: 311 additions & 71 deletions

File tree

src/authz/index.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
2-
import { PreExecutionRule, UnauthorizedError } from "@graphql-authz/core";
2+
import {
3+
PreExecutionRule,
4+
UnauthorizedError,
5+
PostExecutionRule,
6+
} from "@graphql-authz/core";
37
import { GraphQLError } from "graphql";
48

9+
import { UserLoadable } from "~/schema/user/types";
510
import { GraphqlContext } from "~/types";
611

712
import { authHelpers } from "./helpers";
@@ -173,3 +178,28 @@ export class canApproveTicket extends PreExecutionRule {
173178
return Boolean(isEventAdmin);
174179
}
175180
}
181+
182+
export class CanSeePersonalData extends PostExecutionRule {
183+
public async execute(
184+
ctx: GraphqlContext,
185+
fieldArgs: any,
186+
_: any,
187+
parent: {
188+
id: string;
189+
},
190+
) {
191+
if (!ctx.USER) {
192+
return false;
193+
}
194+
195+
if (ctx.USER.isSuperAdmin) {
196+
return true;
197+
}
198+
199+
const loadedUser = await UserLoadable.getDataloader(ctx).load(parent.id);
200+
201+
return loadedUser?.id === ctx.USER.id;
202+
}
203+
204+
selectionSet = `{ id }`;
205+
}

src/datasources/db/schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// TABLES/RELATIONS/CRUD
21
export * from "~/datasources/db/allowedCurrencies";
32

43
export * from "~/datasources/db/communities";
@@ -45,6 +44,8 @@ export * from "~/datasources/db/users";
4544

4645
export * from "~/datasources/db/usersCommunities";
4746

47+
export * from "~/datasources/db/usersData";
48+
4849
export * from "~/datasources/db/usersTags";
4950

5051
export * from "~/datasources/db/userTeams";

src/datasources/db/users.ts

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
import { relations } from "drizzle-orm";
2-
import {
3-
jsonb,
4-
boolean,
5-
pgTable,
6-
text,
7-
uuid,
8-
uniqueIndex,
9-
} from "drizzle-orm/pg-core";
2+
import { boolean, jsonb, pgTable, text, uuid } from "drizzle-orm/pg-core";
103
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
114
import { z } from "zod";
125

136
import {
7+
userDataSchema,
8+
usersToCommunitiesSchema,
149
userTeamsSchema,
1510
userTicketsSchema,
16-
usersToCommunitiesSchema,
1711
} from "./schema";
1812
import {
1913
createdAndUpdatedAtFields,
@@ -107,29 +101,3 @@ export const allowedUserUpdateForAuth = insertUsersSchema
107101
status: true,
108102
})
109103
.partial();
110-
111-
export const userDataSchema = pgTable(
112-
"user_data",
113-
{
114-
id: uuid("id").primaryKey().notNull().defaultRandom(),
115-
userId: uuid("user_id").references(() => usersSchema.id),
116-
countryOfResidence: text("country_of_residence").notNull(),
117-
city: text("city").notNull(),
118-
worksInOrganization: boolean("works_in_organization").notNull(),
119-
organizationName: text("organization_name"),
120-
roleInOrganization: text("role_in_organization"),
121-
...createdAndUpdatedAtFields,
122-
},
123-
(table) => ({
124-
userIdIndex: uniqueIndex("user_id_index").on(table.userId),
125-
}),
126-
);
127-
128-
export const userDataRelations = relations(userDataSchema, ({ one }) => ({
129-
user: one(usersSchema, {
130-
fields: [userDataSchema.userId],
131-
references: [usersSchema.id],
132-
}),
133-
}));
134-
135-
export const selectUserDataSchema = createSelectSchema(userDataSchema);

src/datasources/db/usersData.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { relations } from "drizzle-orm";
2+
import { boolean, pgTable, text, uniqueIndex, uuid } from "drizzle-orm/pg-core";
3+
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
4+
5+
import { usersSchema } from "./schema";
6+
import { createdAndUpdatedAtFields } from "./shared";
7+
8+
export const userDataSchema = pgTable(
9+
"user_data",
10+
{
11+
id: uuid("id").primaryKey().notNull().defaultRandom(),
12+
userId: uuid("user_id").references(() => usersSchema.id),
13+
countryOfResidence: text("country_of_residence").notNull(),
14+
city: text("city").notNull(),
15+
worksInOrganization: boolean("works_in_organization").notNull(),
16+
organizationName: text("organization_name"),
17+
roleInOrganization: text("role_in_organization"),
18+
...createdAndUpdatedAtFields,
19+
},
20+
(table) => ({
21+
userIdIndex: uniqueIndex("user_id_index").on(table.userId),
22+
}),
23+
);
24+
25+
export const userDataRelations = relations(userDataSchema, ({ one }) => ({
26+
user: one(usersSchema, {
27+
fields: [userDataSchema.userId],
28+
references: [usersSchema.id],
29+
}),
30+
}));
31+
32+
export const selectUserDataSchema = createSelectSchema(userDataSchema);
33+
34+
export const insertUserDataSchema = createInsertSchema(userDataSchema);
35+
36+
export const updateUserDataSchema = insertUserDataSchema.pick({
37+
city: true,
38+
countryOfResidence: true,
39+
organizationName: true,
40+
roleInOrganization: true,
41+
worksInOrganization: true,
42+
});

src/schema/invitations/mutations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ builder.mutationField("giftTicketsToUsers", (t) =>
110110
userId,
111111
ticketTemplateId: ticket.id,
112112
purchaseOrderId: purchaseOrder.id,
113+
approvalStatus: "approved",
113114
}),
114115
),
115116
)

src/schema/user/mutations.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import slugify from "slugify";
44

55
import { builder } from "~/builder";
66
import {
7+
insertUserDataSchema,
78
insertUsersSchema,
89
PronounsEnum,
910
selectUsersSchema,
11+
updateUserDataSchema,
1012
updateUsersSchema,
1113
userDataSchema,
1214
usersSchema,
1315
usersToCommunitiesSchema,
1416
} from "~/datasources/db/schema";
17+
import { applicationError, ServiceErrors } from "~/errors";
1518
import { UserRef } from "~/schema/shared/refs";
1619
import { pronounsEnum } from "~/schema/user/types";
1720
import { usersFetcher } from "~/schema/user/userFetcher";
@@ -182,51 +185,60 @@ builder.mutationField("updateMyUserData", (t) =>
182185
input: t.arg({ type: updateUserDataInput, required: true }),
183186
},
184187
resolve: async (root, { input }, ctx) => {
185-
try {
186-
const {
187-
countryOfResidence,
188-
city,
189-
worksInOrganization,
190-
organizationName,
191-
roleInOrganization,
192-
} = input;
188+
const {
189+
countryOfResidence,
190+
city,
191+
worksInOrganization,
192+
organizationName,
193+
roleInOrganization,
194+
} = input;
193195

194-
const USER = ctx.USER;
196+
const USER = ctx.USER;
195197

196-
if (!USER) {
197-
throw new Error("User not found");
198-
}
198+
if (!USER) {
199+
throw new Error("User not found");
200+
}
201+
202+
const userData = insertUserDataSchema.parse({
203+
userId: USER.id,
204+
countryOfResidence,
205+
city,
206+
worksInOrganization,
207+
organizationName,
208+
roleInOrganization,
209+
});
199210

200-
await ctx.DB.insert(userDataSchema)
201-
.values({
202-
userId: USER.id,
211+
const updatedUsers = await ctx.DB.insert(userDataSchema)
212+
.values(userData)
213+
.onConflictDoUpdate({
214+
target: userDataSchema.userId,
215+
set: updateUserDataSchema.parse({
203216
countryOfResidence,
204217
city,
205218
worksInOrganization,
206219
organizationName,
207220
roleInOrganization,
208-
})
209-
.onConflictDoUpdate({
210-
target: userDataSchema.userId,
211-
set: {
212-
countryOfResidence,
213-
city,
214-
worksInOrganization,
215-
organizationName,
216-
roleInOrganization,
217-
},
218-
});
219-
220-
const user = await ctx.DB.query.usersSchema.findFirst({
221-
where: (u, { eq }) => eq(u.id, USER.id),
222-
});
221+
}),
222+
})
223+
.returning();
224+
const updatedUser = updatedUsers[0];
223225

224-
return selectUsersSchema.parse(user);
225-
} catch (e) {
226-
throw new GraphQLError(
227-
e instanceof Error ? e.message : "Unknown error",
226+
if (!updatedUser) {
227+
throw applicationError(
228+
"Could not update the user information",
229+
ServiceErrors.NOT_FOUND,
230+
ctx.logger,
228231
);
229232
}
233+
234+
const user = await usersFetcher.searchUsers({
235+
DB: ctx.DB,
236+
search: {
237+
userIds: [USER.id],
238+
},
239+
});
240+
241+
return selectUsersSchema.parse(user[0]);
230242
},
231243
}),
232244
);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-disable */
2+
/* @ts-nocheck */
3+
/* prettier-ignore */
4+
/* This file is automatically generated using `npm run graphql:types` */
5+
import type * as Types from '../../../../generated/types';
6+
7+
import type { JsonObject } from "type-fest";
8+
import gql from 'graphql-tag';
9+
export type UpdateMyUserDataMutationVariables = Types.Exact<{
10+
input: Types.UpdateUserDataInput;
11+
}>;
12+
13+
14+
export type UpdateMyUserDataMutation = { __typename?: 'Mutation', updateMyUserData: { __typename?: 'User', id: string, userData: { __typename?: 'UserData', city: string, countryOfResidence: string, organizationName: string | null, roleInOrganization: string | null, worksInOrganization: boolean } | null } };
15+
16+
17+
export const UpdateMyUserData = gql`
18+
mutation UpdateMyUserData($input: updateUserDataInput!) {
19+
updateMyUserData(input: $input) {
20+
id
21+
userData {
22+
city
23+
countryOfResidence
24+
organizationName
25+
roleInOrganization
26+
worksInOrganization
27+
}
28+
}
29+
}
30+
`;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
mutation UpdateMyUserData($input: updateUserDataInput!) {
2+
updateMyUserData(input: $input) {
3+
id
4+
userData {
5+
city
6+
countryOfResidence
7+
organizationName
8+
roleInOrganization
9+
worksInOrganization
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)