feat(infisical-pg): added routes for workspaces and updated frontend

This commit is contained in:
Akhil Mohan
2023-12-15 14:05:31 +05:30
parent 498a90c484
commit 4f3582a98b
31 changed files with 953 additions and 132 deletions

View File

@@ -10,6 +10,11 @@ import { AuthMode } from "@app/services/auth/auth-signup-type";
import { ActorType } from "@app/services/auth/auth-type";
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { TAuthTokenServiceFactory } from "@app/services/token/token-service";
import { TUserDalFactory } from "@app/services/user/user-dal";
@@ -54,6 +59,11 @@ declare module "fastify" {
superAdmin: TSuperAdminServiceFactory;
user: TUserServiceFactory;
apiKey: TApiKeyServiceFactory;
project: TProjectServiceFactory;
projectMembership: TProjectMembershipServiceFactory;
projectEnv: TProjectEnvServiceFactory;
projectKey: TProjectKeyServiceFactory;
projectRole: TProjectRoleServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data

View File

@@ -1,6 +1,8 @@
import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router";
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerProjectRoleRouter, { prefix: "/workspace" });
};

View File

@@ -16,7 +16,6 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
slug: z.string().trim(),
name: z.string().trim(),
description: z.string().trim().optional(),
workspaceId: z.string().trim().optional(),
permissions: z.any().array()
}),
response: {
@@ -62,10 +61,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.auth.userId,
req.params.organizationId,
req.params.roleId,
{
...req.body,
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined
}
req.body
);
return { role };
}

View File

@@ -0,0 +1,149 @@
import { z } from "zod";
import { ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectId/roles",
schema: {
params: z.object({
projectId: z.string().trim()
}),
body: z.object({
slug: z.string().trim(),
name: z.string().trim(),
description: z.string().trim().optional(),
permissions: z.any().array()
}),
response: {
200: z.object({
role: ProjectRolesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const role = await server.services.projectRole.createRole(
req.permission.type,
req.permission.id,
req.params.projectId,
req.body
);
return { role };
}
});
server.route({
method: "PATCH",
url: "/:projectId/roles/:roleId",
schema: {
params: z.object({
projectId: z.string().trim(),
roleId: z.string().trim()
}),
body: z.object({
slug: z.string().trim().optional(),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
permissions: z.any().array()
}),
response: {
200: z.object({
role: ProjectRolesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const role = await server.services.projectRole.updateRole(
req.permission.type,
req.permission.id,
req.params.projectId,
req.params.roleId,
req.body
);
return { role };
}
});
server.route({
method: "DELETE",
url: "/:projectId/roles/:roleId",
schema: {
params: z.object({
projectId: z.string().trim(),
roleId: z.string().trim()
}),
response: {
200: z.object({
role: ProjectRolesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const role = await server.services.projectRole.deleteRole(
req.permission.type,
req.permission.id,
req.params.projectId,
req.params.roleId
);
return { role };
}
});
server.route({
method: "GET",
url: "/:projectId/roles",
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
data: z.object({
roles: ProjectRolesSchema.omit({ permissions: true })
.merge(z.object({ permissions: z.unknown() }))
.array()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const roles = await server.services.projectRole.listRoles(
req.permission.type,
req.permission.id,
req.params.projectId
);
return { data: { roles } };
}
});
server.route({
method: "GET",
url: "/:projectId/permissions",
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema,
permissions: z.any().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { permissions, membership } = await server.services.projectRole.getUserPermission(
req.auth.userId,
req.params.projectId
);
return { permissions, membership };
}
});
};

View File

@@ -16,7 +16,9 @@ import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
projectViewerPermission} from "./project-permission";
ProjectPermissionSet,
projectViewerPermission
} from "./project-permission";
type TPermissionServiceFactoryDep = {
permissionDal: TPermissionDalFactory;
@@ -68,9 +70,9 @@ export const permissionServiceFactory = ({ permissionDal }: TPermissionServiceFa
if (membership.role === ProjectMembershipRole.NoAccess)
return { permission: projectNoAccessPermissions, membership };
if (membership.role === ProjectMembershipRole.Custom) {
const permission = createMongoAbility<OrgPermissionSet>(
const permission = createMongoAbility<ProjectPermissionSet>(
// akhilmhdh: putting any due to ts incompatiable matching with string and the other
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(membership.permissions as any),
unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(membership.permissions as any),
{
conditionsMatcher
}

View File

@@ -18,6 +18,16 @@ import { orgDalFactory } from "@app/services/org/org-dal";
import { orgRoleDalFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service";
import { projectDalFactory } from "@app/services/project/project-dal";
import { projectServiceFactory } from "@app/services/project/project-service";
import { projectEnvDalFactory } from "@app/services/project-env/project-env-dal";
import { projectEnvServiceFactory } from "@app/services/project-env/project-env-service";
import { projectKeyDalFactory } from "@app/services/project-key/project-key-dal";
import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service";
import { projectMembershipDalFactory } from "@app/services/project-membership/project-membership-dal";
import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
import { projectRoleDalFactory } from "@app/services/project-role/project-role-dal";
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { superAdminDalFactory } from "@app/services/super-admin/super-admin-dal";
import { superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
@@ -43,6 +53,12 @@ export const registerRoutes = async (
const superAdminDal = superAdminDalFactory(db);
const apiKeyDal = apiKeyDalFactory(db);
const projectDal = projectDalFactory(db);
const projectMembershipDal = projectMembershipDalFactory(db);
const projectRoleDal = projectRoleDalFactory(db);
const projectEnvDal = projectEnvDalFactory(db);
const projectKeyDal = projectKeyDalFactory(db);
// ee db layer ops
const permissionDal = permissionDalFactory(db);
@@ -84,6 +100,30 @@ export const registerRoutes = async (
});
const apiKeyService = apiKeyServiceFactory({ apiKeyDal });
const projectService = projectServiceFactory({
permissionService,
projectDal,
projectEnvDal,
projectMembershipDal
});
const projectMembershipService = projectMembershipServiceFactory({
projectMembershipDal,
projectDal,
permissionService,
orgDal,
userDal,
smtpService,
projectKeyDal,
projectRoleDal
});
const projectEnvService = projectEnvServiceFactory({ permissionService, projectEnvDal });
const projectKeyService = projectKeyServiceFactory({
permissionService,
projectKeyDal,
projectMembershipDal
});
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDal });
await superAdminService.initServerCfg();
// inject all services
server.decorate<FastifyZodProvider["services"]>("services", {
@@ -96,7 +136,12 @@ export const registerRoutes = async (
orgRole: orgRoleService,
apiKey: apiKeyService,
authToken: tokenService,
superAdmin: superAdminService
superAdmin: superAdminService,
project: projectService,
projectMembership: projectMembershipService,
projectKey: projectKeyService,
projectEnv: projectEnvService,
projectRole: projectRoleService
});
server.decorate<FastifyZodProvider["store"]>("store", {

View File

@@ -3,6 +3,10 @@ import { registerAuthRoutes } from "./auth-router";
import { registerInviteOrgRouter } from "./invite-org-router";
import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router";
import { registerProjectEnvRouter } from "./project-env-router";
import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router";
import { registerSsoRouter } from "./sso-router";
import { registerUserActionRouter } from "./user-action-router";
import { registerUserRouter } from "./user-router";
@@ -10,11 +14,20 @@ import { registerUserRouter } from "./user-router";
export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSsoRouter, { prefix: "/sso" });
await server.register(registerAuthRoutes, { prefix: "/auth" });
// await server.register(registerSSORouter, { prefix: "/sso" });
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerUserRouter, { prefix: "/user" });
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
await server.register(registerUserActionRouter, { prefix: "/user-action" });
await server.register(
async (projectServer) => {
await projectServer.register(registerProjectRouter);
await projectServer.register(registerProjectEnvRouter);
await projectServer.register(registerProjectKeyRouter);
await projectServer.register(registerProjectMembershipRouter);
},
{ prefix: "/workspace" }
);
};

View File

@@ -0,0 +1,111 @@
import { z } from "zod";
import { ProjectEnvironmentsSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:workspaceId/environments",
method: "POST",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
name: z.string().trim(),
slug: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
workspace: z.string(),
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const environment = await server.services.projectEnv.createEnvironment({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
...req.body
});
return {
message: "Successfully created new environment",
workspace: req.params.workspaceId,
environment
};
}
});
server.route({
url: "/:workspaceId/environments/:id",
method: "PATCH",
schema: {
params: z.object({
workspaceId: z.string().trim(),
id: z.string().trim()
}),
body: z.object({
slug: z.string().trim().optional(),
name: z.string().trim().optional()
}),
response: {
200: z.object({
message: z.string(),
workspace: z.string(),
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const environment = await server.services.projectEnv.updateEnvironment({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
id: req.params.id,
...req.body
});
return {
message: "Successfully updated environment",
workspace: req.params.workspaceId,
environment
};
}
});
server.route({
url: "/:workspaceId/environments/:id",
method: "DELETE",
schema: {
params: z.object({
workspaceId: z.string().trim(),
id: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
workspace: z.string(),
environment: ProjectEnvironmentsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const environment = await server.services.projectEnv.deleteEnvironment({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
id: req.params.id
});
return {
message: "Successfully deleted environment",
workspace: req.params.workspaceId,
environment
};
}
});
};

View File

@@ -0,0 +1,40 @@
import { z } from "zod";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectKeyRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:workspaceId/key",
method: "POST",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
key: z.object({
encryptedKey: z.string().trim(),
nonce: z.string().trim(),
userId: z.string().trim()
})
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.projectKey.uploadProjectKeys({
projectId: req.params.workspaceId,
actor: req.permission.type,
actorId: req.permission.id,
nonce: req.body.key.nonce,
receiverId: req.body.key.userId,
encryptedKey: req.body.key.encryptedKey
});
return { message: "Successfully uploaded key to workspace" };
}
});
};

View File

@@ -0,0 +1,100 @@
import { z } from "zod";
import { ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectMembershipRouter = async (server: FastifyZodProvider) => {
// TODO(akhilmhdh-pg): missing adding multiple user workspace refer v2/membership
server.route({
url: "/:workspaceId/memberships",
method: "GET",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
memberships: ProjectMembershipsSchema.merge(
z.object({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
})
)
.omit({ createdAt: true, updatedAt: true })
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const memberships = await server.services.projectMembership.getProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId
});
return { memberships };
}
});
server.route({
url: "/:workspaceId/memberships/:membershipId",
method: "PATCH",
schema: {
params: z.object({
workspaceId: z.string().trim(),
membershipId: z.string().trim()
}),
body: z.object({
role: z.string().trim()
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const membership = await server.services.projectMembership.updateProjectMembership({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
membershipId: req.params.membershipId,
role: req.body.role
});
return { membership };
}
});
server.route({
url: "/:workspaceId/memberships/:membershipId",
method: "DELETE",
schema: {
params: z.object({
workspaceId: z.string().trim(),
membershipId: z.string().trim()
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const membership = await server.services.projectMembership.deleteProjectMembership({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
membershipId: req.params.membershipId
});
return { membership };
}
});
};

View File

@@ -0,0 +1,265 @@
import { z } from "zod";
import {
ProjectKeysSchema,
ProjectMembershipsSchema,
ProjectsSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerProjectRouter = (server: FastifyZodProvider) => {
server.route({
url: "/:workspaceId/keys",
method: "GET",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
publicKeys: z
.object({
publicKey: z.string().optional(),
userId: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const publicKeys = await server.services.projectKey.getProjectPublicKeys({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId
});
return { publicKeys };
}
});
server.route({
url: "/:workspaceId/users",
method: "GET",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
users: ProjectMembershipsSchema.merge(
z.object({
user: UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
id: true
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
})
)
.omit({ createdAt: true, updatedAt: true })
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const users = await server.services.projectMembership.getProjectMemberships({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId
});
return { users };
}
});
server.route({
url: "/",
method: "GET",
schema: {
response: {
200: z.object({
workspaces: ProjectsSchema.merge(
z.object({ environments: z.object({ name: z.string(), slug: z.string() }).array() })
).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const workspaces = await server.services.project.getProjects(req.permission.id);
return { workspaces };
}
});
server.route({
url: "/:workspaceId",
method: "GET",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
workspace: ProjectsSchema.merge(
z.object({ environments: z.object({ name: z.string(), slug: z.string() }).array() })
).optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.getAProject({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId
});
return { workspace };
}
});
server.route({
url: "/",
method: "POST",
schema: {
body: z.object({
workspaceName: z.string().trim(),
organizationId: z.string().trim()
}),
response: {
200: z.object({
workspace: ProjectsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.createProject({
actorId: req.permission.id,
actor: req.permission.type,
orgId: req.body.organizationId,
workspaceName: req.body.workspaceName
});
return { workspace };
}
});
server.route({
url: "/:workspaceId",
method: "DELETE",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
workspace: ProjectsSchema.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.deleteProject({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId
});
return { workspace };
}
});
server.route({
url: "/:workspaceId/name",
method: "POST",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
name: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
workspace: ProjectsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.updateName({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
name: req.body.name
});
return {
message: "Successfully changed workspace name",
workspace
};
}
});
server.route({
url: "/:workspaceId/auto-capitalization",
method: "POST",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
autoCapitalization: z.boolean()
}),
response: {
200: z.object({
message: z.string(),
workspace: ProjectsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.toggleAutoCapitalization({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
autoCapitalization: req.body.autoCapitalization
});
return {
message: "Successfully changed workspace settings",
workspace
};
}
});
server.route({
url: "/:workspaceId/invite-signup",
method: "POST",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
email: z.string().trim()
}),
response: {
200: z.object({
invitee: UsersSchema,
latestKey: ProjectKeysSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { invitee, latestKey } = await server.services.projectMembership.inviteUserToProject({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId,
email: req.body.email
});
return { invitee, latestKey };
}
});
};

View File

@@ -1,7 +1,14 @@
import { registerMfaRouter } from "./mfa-router";
import { registerProjectRouter } from "./project-router";
import { registerUserRouter } from "./user-router";
export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerMfaRouter, { prefix: "/auth" });
await server.register(registerUserRouter, { prefix: "/users" });
await server.register(
async (projectServer) => {
projectServer.register(registerProjectRouter);
},
{ prefix: "/workspace" }
);
};

View File

@@ -0,0 +1,35 @@
import { ProjectKeysSchema } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { z } from "zod";
export const registerProjectRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/:workspaceId/encrypted-key",
method: "GET",
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: ProjectKeysSchema.merge(
z.object({
sender: z.object({
publicKey: z.string()
})
})
)
}
},
onResponse: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => {
const key = await server.services.projectKey.getLatestProjectKey({
actor: req.permission.type,
actorId: req.permission.id,
projectId: req.params.workspaceId
});
return key;
}
});
};

View File

@@ -60,7 +60,10 @@ export const orgRoleServiceFactory = ({
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
}
const [updatedRole] = await orgRoleDal.update({ id: roleId, orgId }, { ...data });
const [updatedRole] = await orgRoleDal.update(
{ id: roleId, orgId },
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
);
if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return updatedRole;
};

View File

@@ -1,5 +1,5 @@
import { TDbClient } from "@app/db";
import { TableName,TProjectKeys } from "@app/db/schemas";
import { TableName, TProjectKeys } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
@@ -35,8 +35,26 @@ export const projectKeyDalFactory = (db: TDbClient) => {
}
};
const findAllProjectUserPubKeys = async (projectId: string) => {
try {
const pubKeys = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(
TableName.UserEncryptionKey,
`${TableName.Users}.id`,
`${TableName.UserEncryptionKey}.userId`
)
.select("userId", "publicKey");
return pubKeys;
} catch (error) {
throw new DatabaseError({ error, name: "Find all workspace pub keys" });
}
};
return {
...projectKeyOrm,
findAllProjectUserPubKeys,
findLatestProjectKey
};
};

View File

@@ -57,8 +57,18 @@ export const projectKeyServiceFactory = ({
return latestKey;
};
const getProjectPublicKeys = async ({ actor, actorId, projectId }: TGetLatestProjectKeyDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Member
);
return projectKeyDal.findAllProjectUserPubKeys(projectId);
};
return {
uploadProjectKeys,
getProjectPublicKeys,
getLatestProjectKey
};
};

View File

@@ -21,20 +21,18 @@ export const projectMembershipDalFactory = (db: TDbClient) => {
)
.select(
db.ref("id").withSchema(TableName.ProjectMembership),
db.ref("inviteEmail").withSchema(TableName.ProjectMembership),
db.ref("orgId").withSchema(TableName.ProjectMembership),
db.ref("projectId").withSchema(TableName.ProjectMembership),
db.ref("role").withSchema(TableName.ProjectMembership),
db.ref("roleId").withSchema(TableName.ProjectMembership),
db.ref("status").withSchema(TableName.ProjectMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
);
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
return members.map(({ email, firstName, lastName, publicKey, ...data }) => ({
...data,
user: { email, firstName, lastName, id: userId, publicKey }
user: { email, firstName, lastName, id: data.userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all project members" });

View File

@@ -177,6 +177,7 @@ export const projectMembershipServiceFactory = ({
tx
);
await projectKeyDal.delete({ receiverId: deletedMembership.userId, projectId }, tx);
return deletedMembership;
});
return membership;
};

View File

@@ -2,15 +2,13 @@ import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { ProjectMembershipRole, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas";
import {
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
ProjectPermissionActions,
ProjectPermissionSub,
projectViewerPermission
} from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
@@ -22,7 +20,7 @@ type TProjectRoleServiceFactoryDep = {
projectRoleDal: TProjectRoleDalFactory;
permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "getUserOrgPermission"
"getProjectPermission" | "getUserProjectPermission"
>;
};
@@ -40,8 +38,8 @@ export const projectRoleServiceFactory = ({
) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Role
ProjectPermissionActions.Create,
ProjectPermissionSub.Role
);
const existingRole = await projectRoleDal.findOne({ slug: data.slug, projectId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
@@ -62,8 +60,8 @@ export const projectRoleServiceFactory = ({
) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Role
ProjectPermissionActions.Edit,
ProjectPermissionSub.Role
);
if (data?.slug) {
const existingRole = await projectRoleDal.findOne({ slug: data.slug, projectId });
@@ -83,8 +81,8 @@ export const projectRoleServiceFactory = ({
) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Role
ProjectPermissionActions.Delete,
ProjectPermissionSub.Role
);
const [deletedRole] = await projectRoleDal.delete({ id: roleId, projectId });
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
@@ -95,8 +93,8 @@ export const projectRoleServiceFactory = ({
const listRoles = async (actor: ActorType, actorId: string, projectId: string) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Role
ProjectPermissionActions.Read,
ProjectPermissionSub.Role
);
const customRoles = await projectRoleDal.find({ projectId });
const roles = [
@@ -149,8 +147,11 @@ export const projectRoleServiceFactory = ({
return roles;
};
const getUserPermission = async (userId: string, orgId: string) => {
const { permission, membership } = await permissionService.getUserOrgPermission(userId, orgId);
const getUserPermission = async (userId: string, projectId: string) => {
const { permission, membership } = await permissionService.getUserProjectPermission(
userId,
projectId
);
return { permissions: packRules(permission.rules), membership };
};

View File

@@ -1,5 +1,5 @@
import { TDbClient } from "@app/db";
import { TableName,TProjects } from "@app/db/schemas";
import { TableName, TProjects } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { mergeOneToManyRelation, ormify } from "@app/lib/knex";
@@ -90,27 +90,9 @@ export const projectDalFactory = (db: TDbClient) => {
}
};
const findAllProjectUserPubKeys = async (projectId: string) => {
try {
const pubKeys = await db(TableName.ProjectMembership)
.where({ projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(
TableName.UserEncryptionKey,
`${TableName.Users}.id`,
`${TableName.UserEncryptionKey}.userId`
)
.select("userId", "publicKey");
return pubKeys;
} catch (error) {
throw new DatabaseError({ error, name: "Find all workspace pub keys" });
}
};
return {
...projectOrm,
findAllProjects,
findProjectById,
findAllProjectUserPubKeys
findProjectById
};
};

View File

@@ -103,11 +103,28 @@ export const projectServiceFactory = ({
return updatedProject;
};
const updateName = async ({
projectId,
actor,
actorId,
name
}: TGetProjectDTO & { name: string }) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Settings
);
const updatedProject = await projectDal.updateById(projectId, { name });
return updatedProject;
};
return {
createProject,
deleteProject,
getProjects,
getAProject,
toggleAutoCapitalization
toggleAutoCapitalization,
updateName
};
};

View File

@@ -60,11 +60,12 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
// Delete the row in the table (e.g. a user)
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
const handleDelete = async (membershipId: string) => {
await deleteUserFromWorkspaceMutateAsync(membershipId);
await deleteUserFromWorkspaceMutateAsync({ membershipId, workspaceId });
};
const handleRoleUpdate = async (index: number, e: string) => {
await updateUserWorkspaceRoleMutateAsync({
workspaceId,
membershipId: userData[index].membershipId,
role: e.toLowerCase()
});

View File

@@ -9,11 +9,11 @@ const encKeyKeys = {
};
export const fetchUserWsKey = async (workspaceID: string) => {
const { data } = await apiRequest.get<{ latestKey: UserWsKeyPair }>(
`/api/v1/key/${workspaceID}/latest`
const { data } = await apiRequest.get<UserWsKeyPair>(
`/api/v2/workspace/${workspaceID}/encrypted-key`
);
return data.latestKey;
return data;
};
export const useGetUserWsKey = (workspaceID: string) =>
@@ -24,14 +24,11 @@ export const useGetUserWsKey = (workspaceID: string) =>
});
// mutations
export const uploadWsKey = async ({
workspaceId,
userId,
encryptedKey,
nonce
}: UploadWsKeyDTO) => {
return apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } })
}
export const uploadWsKey = async ({ workspaceId, userId, encryptedKey, nonce }: UploadWsKeyDTO) => {
return apiRequest.post(`/api/v1/workspace/${workspaceId}/key`, {
key: { userId, encryptedKey, nonce }
});
};
export const useUploadWsKey = () =>
useMutation<{}, {}, UploadWsKeyDTO>({

View File

@@ -31,7 +31,8 @@ export const workspaceKeys = {
getAllUserWorkspace: ["workspaces"] as const,
getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const,
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const,
getWorkspaceIdentityMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-identity-memberships"] as const
getWorkspaceIdentityMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-identity-memberships"] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
@@ -96,7 +97,7 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => {
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
`/api/v1/organization/${orgId}/workspace-memberships`
);
return data;
};
@@ -221,10 +222,10 @@ export const useCreateWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, CreateEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentName, environmentSlug }) => {
return apiRequest.post(`/api/v2/workspace/${workspaceID}/environments`, {
environmentName,
environmentSlug
mutationFn: ({ workspaceId, name, slug }) => {
return apiRequest.post(`/api/v1/workspace/${workspaceId}/environments`, {
name,
slug
});
},
onSuccess: () => {
@@ -238,13 +239,13 @@ export const useReorderWsEnvironment = () => {
return useMutation<{}, {}, ReorderEnvironmentsDTO>({
mutationFn: ({
workspaceID,
workspaceId,
environmentSlug,
environmentName,
otherEnvironmentSlug,
otherEnvironmentName
}) => {
return apiRequest.patch(`/api/v2/workspace/${workspaceID}/environments`, {
return apiRequest.patch(`/api/v2/workspace/${workspaceId}/environments`, {
environmentSlug,
environmentName,
otherEnvironmentSlug,
@@ -261,11 +262,10 @@ export const useUpdateWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentName, environmentSlug, oldEnvironmentSlug }) => {
return apiRequest.put(`/api/v2/workspace/${workspaceID}/environments`, {
environmentName,
environmentSlug,
oldEnvironmentSlug
mutationFn: ({ workspaceId, id, name, slug }) => {
return apiRequest.put(`/api/v2/workspace/${workspaceId}/environments/${id}`, {
name,
slug
});
},
onSuccess: () => {
@@ -278,10 +278,8 @@ export const useDeleteWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentSlug }) => {
return apiRequest.delete(`/api/v2/workspace/${workspaceID}/environments`, {
data: { environmentSlug }
});
mutationFn: ({ id, workspaceId }) => {
return apiRequest.delete(`/api/v1/workspace/${workspaceId}/environments/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
@@ -328,14 +326,20 @@ export const useDeleteUserFromWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (membershipId: string) => {
mutationFn: async ({
membershipId,
workspaceId
}: {
workspaceId: string;
membershipId: string;
}) => {
const {
data: { deletedMembership }
} = await apiRequest.delete(`/api/v1/membership/${membershipId}`);
} = await apiRequest.delete(`/api/v1/${workspaceId}/membership/${membershipId}`);
return deletedMembership;
},
onSuccess: (res) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(res.workspace));
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId));
}
});
};
@@ -343,10 +347,18 @@ export const useDeleteUserFromWorkspace = () => {
export const useUpdateUserWorkspaceRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ membershipId, role }: { membershipId: string; role: string }) => {
mutationFn: async ({
membershipId,
role,
workspaceId
}: {
membershipId: string;
role: string;
workspaceId: string;
}) => {
const {
data: { membership }
} = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, {
} = await apiRequest.post(`/api/v1/${workspaceId}/membership/${membershipId}`, {
role
});
return membership;
@@ -369,13 +381,15 @@ export const useAddIdentityToWorkspace = () => {
workspaceId: string;
role?: string;
}) => {
const {
data: { identityMembership }
} = await apiRequest.post(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`, {
role
});
} = await apiRequest.post(
`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`,
{
role
}
);
return identityMembership;
},
onSuccess: (_, { workspaceId }) => {
@@ -396,12 +410,14 @@ export const useUpdateIdentityWorkspaceRole = () => {
workspaceId: string;
role?: string;
}) => {
const {
data: { identityMembership }
} = await apiRequest.patch(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`, {
role
});
} = await apiRequest.patch(
`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`,
{
role
}
);
return identityMembership;
},
@@ -416,14 +432,16 @@ export const useDeleteIdentityFromWorkspace = () => {
return useMutation({
mutationFn: async ({
identityId,
workspaceId,
workspaceId
}: {
identityId: string;
workspaceId: string;
}) => {
const {
data: { identityMembership }
} = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`);
} = await apiRequest.delete(
`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`
);
return identityMembership;
},
onSuccess: (_, { workspaceId }) => {
@@ -445,4 +463,4 @@ export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => {
},
enabled: true
});
};
};

View File

@@ -8,6 +8,7 @@ export type Workspace = {
};
export type WorkspaceEnv = {
id: string;
name: string;
slug: string;
isReadDenied: boolean;
@@ -36,13 +37,13 @@ export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean
export type DeleteWorkspaceDTO = { workspaceID: string };
export type CreateEnvironmentDTO = {
workspaceID: string;
environmentSlug: string;
environmentName: string;
workspaceId: string;
name: string;
slug: string;
};
export type ReorderEnvironmentsDTO = {
workspaceID: string;
workspaceId: string;
environmentSlug: string;
environmentName: string;
otherEnvironmentSlug: string;
@@ -50,10 +51,10 @@ export type ReorderEnvironmentsDTO = {
};
export type UpdateEnvironmentDTO = {
workspaceID: string;
oldEnvironmentSlug: string;
environmentSlug: string;
environmentName: string;
workspaceId: string;
id: string;
name?: string;
slug?: string;
};
export type DeleteEnvironmentDTO = { workspaceID: string; environmentSlug: string };
export type DeleteEnvironmentDTO = { workspaceId: string; id: string };

View File

@@ -142,7 +142,7 @@ export const MemberListTab = () => {
if (!currentOrg?.id) return;
try {
await removeUserFromWorkspace(membershipId);
await removeUserFromWorkspace({ workspaceId, membershipId });
createNotification({
text: "Successfully removed user from workspace",
type: "success"
@@ -182,7 +182,7 @@ export const MemberListTab = () => {
return;
}
await updateUserWorkspaceRole({ membershipId, role });
await updateUserWorkspaceRole({ membershipId, role, workspaceId });
createNotification({
text: "Successfully updated user role",
type: "success"

View File

@@ -34,9 +34,9 @@ export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle
if (!currentWorkspace?.id) return;
await mutateAsync({
workspaceID: currentWorkspace.id,
environmentName,
environmentSlug
workspaceId: currentWorkspace.id,
name: environmentName,
slug: environmentSlug
});
createNotification({

View File

@@ -38,13 +38,13 @@ export const EnvironmentSection = () => {
"upgradePlan"
] as const);
const onEnvDeleteSubmit = async (environmentSlug: string) => {
const onEnvDeleteSubmit = async (id: string) => {
try {
if (!currentWorkspace?.id) return;
await deleteWsEnvironment.mutateAsync({
workspaceID: currentWorkspace.id,
environmentSlug
workspaceId: currentWorkspace.id,
id
});
createNotification({
@@ -116,9 +116,7 @@ export const EnvironmentSection = () => {
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteEnv", isOpen)}
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ""}
onDeleteApproved={() =>
onEnvDeleteSubmit((popUp?.deleteEnv?.data as { slug: string })?.slug)
}
onDeleteApproved={() => onEnvDeleteSubmit((popUp?.deleteEnv?.data as { id: string })?.id)}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}

View File

@@ -24,10 +24,12 @@ type Props = {
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
{
name,
slug
slug,
id
}: {
name: string;
slug: string;
id: string;
}
) => void;
};
@@ -56,7 +58,7 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
const indexToSwap = shouldMoveUp ? indexOfEnv - 1 : indexOfEnv + 1;
await reorderWsEnvironment.mutateAsync({
workspaceID: currentWorkspace.id,
workspaceId: currentWorkspace.id,
environmentSlug: slug,
environmentName: name,
otherEnvironmentSlug: currentWorkspace.environments[indexToSwap].slug,
@@ -90,7 +92,7 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
{isLoading && <TableSkeleton columns={3} innerKey="project-envs" />}
{!isLoading &&
currentWorkspace &&
currentWorkspace.environments.map(({ name, slug }, pos) => (
currentWorkspace.environments.map(({ name, slug, id }, pos) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
@@ -142,7 +144,7 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
<IconButton
className="mr-3 py-2"
onClick={() => {
handlePopUpOpen("updateEnv", { name, slug });
handlePopUpOpen("updateEnv", { name, slug, id });
}}
isDisabled={!isAllowed}
colorSchema="primary"
@@ -160,7 +162,7 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteEnv", { name, slug });
handlePopUpOpen("deleteEnv", { name, slug, id });
}}
size="lg"
colorSchema="danger"

View File

@@ -29,17 +29,17 @@ export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpTog
resolver: yupResolver(schema)
});
const oldEnvironmentSlug = (popUp?.updateEnv?.data as { slug: string })?.slug;
const oldEnvId = (popUp?.updateEnv?.data as { id: string })?.id;
const onFormSubmit = async ({ environmentName, environmentSlug }: FormData) => {
try {
if (!currentWorkspace?.id) return;
await mutateAsync({
workspaceID: currentWorkspace.id,
environmentName,
environmentSlug,
oldEnvironmentSlug
workspaceId: currentWorkspace.id,
name: environmentName,
slug: environmentSlug,
id: oldEnvId
});
createNotification({

View File

@@ -23,8 +23,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"jsx": "preserve"
},
"include": [
"next-i18next.config.js",