Skip to content
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

feat: allow org members to mark projects as favorites #2040

Merged
merged 10 commits into from
Jul 1, 2024
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
);
}
});
};
52 changes: 50 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,49 @@ 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 memberProjectFavorites = (
await projectMembershipDAL.find({
userId,
$in: {
projectId: projectIds
}
})
).map((projectMembership) => projectMembership.projectId);

return (
await orgMembershipDAL.updateById(orgMembership.id, {
sheensantoscapadngan marked this conversation as resolved.
Show resolved Hide resolved
projectFavorites: memberProjectFavorites
})
).projectFavorites;
};

return {
sendEmailVerificationCode,
verifyEmailVerificationCode,
Expand All @@ -258,6 +304,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: () => {
queryClient.invalidateQueries(userKeys.userProjectFavorites);
}
});
};
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: ["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,
queryFn: () => fetchUserProjectFavorites(orgId)
});

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