Skip to content

Commit

Permalink
Merge pull request #2040 from Infisical/feat/mark-projects-as-favourite
Browse files Browse the repository at this point in the history
feat: allow org members to mark projects as favorites
  • Loading branch information
sheensantoscapadngan committed Jul 1, 2024
2 parents 178ddf1 + 06a4e68 commit b8f65fc
Show file tree
Hide file tree
Showing 9 changed files with 436 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Knex } from "knex";

import { TableName } from "../schemas";

export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.OrgMembership, "projectFavorites"))) {
await knex.schema.alterTable(TableName.OrgMembership, (tb) => {
tb.specificType("projectFavorites", "text[]");
});
}
}

export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.OrgMembership, "projectFavorites")) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
t.dropColumn("projectFavorites");
});
}
}
3 changes: 2 additions & 1 deletion backend/src/db/schemas/org-memberships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const OrgMembershipsSchema = z.object({
updatedAt: z.date(),
userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid(),
roleId: z.string().uuid().nullable().optional()
roleId: z.string().uuid().nullable().optional(),
projectFavorites: z.string().array().nullable().optional()
});

export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
Expand Down
4 changes: 3 additions & 1 deletion backend/src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,10 @@ export const registerRoutes = async (
userAliasDAL,
orgMembershipDAL,
tokenService,
smtpService
smtpService,
projectMembershipDAL
});

const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
const passwordService = authPaswordServiceFactory({
tokenService,
Expand Down
46 changes: 45 additions & 1 deletion backend/src/server/routes/v1/user-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { z } from "zod";
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
import { authRateLimit, readLimit } from "@app/server/config/rateLimiter";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";

Expand Down Expand Up @@ -90,4 +90,48 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
return res.redirect(`${appCfg.SITE_URL}/login`);
}
});

server.route({
method: "GET",
url: "/me/project-favorites",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
orgId: z.string().trim()
}),
response: {
200: z.object({
projectFavorites: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.user.getUserProjectFavorites(req.permission.id, req.query.orgId);
}
});

server.route({
method: "PUT",
url: "/me/project-favorites",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
orgId: z.string().trim(),
projectFavorites: z.string().array()
})
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.user.updateUserProjectFavorites(
req.permission.id,
req.body.orgId,
req.body.projectFavorites
);
}
});
};
54 changes: 52 additions & 2 deletions backend/src/services/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";

import { AuthMethod } from "../auth/auth-type";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TUserDALFactory } from "./user-dal";

type TUserServiceFactoryDep = {
Expand All @@ -26,8 +27,9 @@ type TUserServiceFactoryDep = {
| "delete"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
smtpService: Pick<TSmtpService, "sendMail">;
};

Expand All @@ -37,6 +39,7 @@ export const userServiceFactory = ({
userDAL,
userAliasDAL,
orgMembershipDAL,
projectMembershipDAL,
tokenService,
smtpService
}: TUserServiceFactoryDep) => {
Expand Down Expand Up @@ -247,6 +250,51 @@ export const userServiceFactory = ({
return privateKey;
};

const getUserProjectFavorites = async (userId: string, orgId: string) => {
const orgMembership = await orgMembershipDAL.findOne({
userId,
orgId
});

if (!orgMembership) {
throw new BadRequestError({
message: "User does not belong in the organization."
});
}

return { projectFavorites: orgMembership.projectFavorites || [] };
};

const updateUserProjectFavorites = async (userId: string, orgId: string, projectIds: string[]) => {
const orgMembership = await orgMembershipDAL.findOne({
userId,
orgId
});

if (!orgMembership) {
throw new BadRequestError({
message: "User does not belong in the organization."
});
}

const matchingUserProjectMemberships = await projectMembershipDAL.find({
userId,
$in: {
projectId: projectIds
}
});

const memberProjectFavorites = matchingUserProjectMemberships.map(
(projectMembership) => projectMembership.projectId
);

const updatedOrgMembership = await orgMembershipDAL.updateById(orgMembership.id, {
projectFavorites: memberProjectFavorites
});

return updatedOrgMembership.projectFavorites;
};

return {
sendEmailVerificationCode,
verifyEmailVerificationCode,
Expand All @@ -258,6 +306,8 @@ export const userServiceFactory = ({
createUserAction,
getUserAction,
unlockUser,
getUserPrivateKey
getUserPrivateKey,
getUserProjectFavorites,
updateUserProjectFavorites
};
};
24 changes: 24 additions & 0 deletions frontend/src/hooks/api/users/mutation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { apiRequest } from "@app/config/request";

import { workspaceKeys } from "../workspace/queries";
import { userKeys } from "./queries";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";

export const useAddUserToWsE2EE = () => {
Expand Down Expand Up @@ -88,3 +89,26 @@ export const useVerifyEmailVerificationCode = () => {
}
});
};

export const useUpdateUserProjectFavorites = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
orgId,
projectFavorites
}: {
orgId: string;
projectFavorites: string[];
}) => {
await apiRequest.put("/api/v1/user/me/project-favorites", {
orgId,
projectFavorites
});

return {};
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(userKeys.userProjectFavorites(orgId));
}
});
};
16 changes: 16 additions & 0 deletions frontend/src/hooks/api/users/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ export const userKeys = {
getUser: ["user"] as const,
getPrivateKey: ["user"] as const,
userAction: ["user-action"] as const,
userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const,

myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
};

Expand Down Expand Up @@ -74,6 +76,14 @@ export const fetchUserAction = async (action: string) => {
return data.userAction || "";
};

export const fetchUserProjectFavorites = async (orgId: string) => {
const { data } = await apiRequest.get<{ projectFavorites: string[] }>(
`/api/v1/user/me/project-favorites?orgId=${orgId}`
);

return data.projectFavorites;
};

export const useRenameUser = () => {
const queryClient = useQueryClient();

Expand Down Expand Up @@ -122,6 +132,12 @@ export const fetchOrgUsers = async (orgId: string) => {
return data.users;
};

export const useGetUserProjectFavorites = (orgId: string) =>
useQuery({
queryKey: userKeys.userProjectFavorites(orgId),
queryFn: () => fetchUserProjectFavorites(orgId)
});

export const useGetOrgUsers = (orgId: string) =>
useQuery({
queryKey: userKeys.getOrgUsers(orgId),
Expand Down
Loading

0 comments on commit b8f65fc

Please sign in to comment.