mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): added secret version api, fixed pass reset flow, smtp connection issue fixed
This commit is contained in:
@@ -8,6 +8,7 @@ import { registerSecretApprovalRequestRouter } from "./secret-approval-request-r
|
||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
||||
import { registerSecretScanningRouter } from "./secret-scanning-router";
|
||||
import { registerSecretVersionRouter } from "./secret-version-router";
|
||||
import { registerSnapshotRouter } from "./snapshot-router";
|
||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||
|
||||
@@ -34,4 +35,5 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSamlRouter, { prefix: "/sso" });
|
||||
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
|
||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||
};
|
||||
|
||||
37
backend-pg/src/ee/routes/v1/secret-version-router.ts
Normal file
37
backend-pg/src/ee/routes/v1/secret-version-router.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretVersionsSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretVersionRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/:secretId/secret-versions",
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
secretId: z.string()
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretVersions = await server.services.secret.getSecretVersions({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
secretId: req.params.secretId
|
||||
});
|
||||
return { secretVersions };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -6,12 +6,12 @@ import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TOrgDalFactory } from "@app/services/org/org-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDalFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TSecretScanningDalFactory } from "../secret-scanning-dal";
|
||||
import {
|
||||
scanContentAndGetFindings,
|
||||
scanFullRepoContentAndGetFindings} from "./secret-scanning-fns";
|
||||
scanFullRepoContentAndGetFindings
|
||||
} from "./secret-scanning-fns";
|
||||
import {
|
||||
SecretMatch,
|
||||
TScanFullRepoEventPayload,
|
||||
@@ -23,7 +23,6 @@ type TSecretScanningQueueFactoryDep = {
|
||||
secretScanningDal: TSecretScanningDalFactory;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
orgMembershipDal: Pick<TOrgDalFactory, "findMembership">;
|
||||
userDal: Pick<TUserDalFactory, "find">;
|
||||
};
|
||||
|
||||
export type TSecretScanningQueueFactory = ReturnType<typeof secretScanningQueueFactory>;
|
||||
@@ -33,7 +32,6 @@ export const secretScanningQueueFactory = ({
|
||||
secretScanningDal,
|
||||
smtpService,
|
||||
orgMembershipDal: orgMemberDal,
|
||||
userDal
|
||||
}: TSecretScanningQueueFactoryDep) => {
|
||||
const startFullRepoScan = async (payload: TScanFullRepoEventPayload) => {
|
||||
await queueService.queue(QueueName.SecretFullRepoScan, QueueJobs.SecretScan, payload, {
|
||||
@@ -69,12 +67,7 @@ export const secretScanningQueueFactory = ({
|
||||
orgId: organizationId,
|
||||
role: OrgMembershipRole.Admin
|
||||
});
|
||||
const userEmails = await userDal.find({
|
||||
$in: {
|
||||
id: adminsOfWork.map(({ userId }) => userId).filter(Boolean) as string[]
|
||||
}
|
||||
});
|
||||
return userEmails.map((userObject) => userObject.email);
|
||||
return adminsOfWork.map((userObject) => userObject.email);
|
||||
};
|
||||
|
||||
queueService.start(QueueName.SecretPushEventScan, async (job) => {
|
||||
|
||||
@@ -251,7 +251,6 @@ export const registerRoutes = async (
|
||||
const apiKeyService = apiKeyServiceFactory({ apiKeyDal, userDal });
|
||||
|
||||
const secretScanningQueue = secretScanningQueueFactory({
|
||||
userDal,
|
||||
smtpService,
|
||||
secretScanningDal,
|
||||
queueService,
|
||||
|
||||
@@ -31,6 +31,8 @@ export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
|
||||
export const secretRawSchema = z.object({
|
||||
id: z.string(),
|
||||
_id: z.string(),
|
||||
workspace: z.string(),
|
||||
environment: z.string(),
|
||||
version: z.number(),
|
||||
type: z.string(),
|
||||
secretKey: z.string(),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BackupPrivateKeySchema } from "@app/db/schemas";
|
||||
import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -66,6 +67,58 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/email/password-reset",
|
||||
schema: {
|
||||
body: z.object({
|
||||
email: z.string().email().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
await server.services.password.sendPasswordResetEmail(req.body.email);
|
||||
|
||||
return {
|
||||
message: "If an account exists with this email, a password reset link has been sent"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/email/password-reset-verify",
|
||||
schema: {
|
||||
body: z.object({
|
||||
email: z.string().email().trim(),
|
||||
code: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
user: UsersSchema,
|
||||
token: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { token, user } = await server.services.password.verifyPasswordResetEmail(
|
||||
req.body.email,
|
||||
req.body.code
|
||||
);
|
||||
|
||||
return {
|
||||
message: "Successfully verified email",
|
||||
user,
|
||||
token
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/backup-private-key",
|
||||
@@ -87,9 +140,10 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const token = validateSignUpAuthorization(req.headers.authorization as string, "",false)!
|
||||
const backupPrivateKey = await server.services.password.createBackupPrivateKey({
|
||||
...req.body,
|
||||
userId: req.permission.id
|
||||
userId: token.userId,
|
||||
});
|
||||
if (!backupPrivateKey) throw new Error("Failed to create backup key");
|
||||
|
||||
@@ -100,7 +154,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/backup-private-key",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -110,8 +163,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const token = validateSignUpAuthorization(req.headers.authorization as string, "",false)!
|
||||
const backupPrivateKey = await server.services.password.getBackupPrivateKeyOfUser(
|
||||
req.permission.id
|
||||
token.userId
|
||||
);
|
||||
if (!backupPrivateKey) throw new Error("Failed to find backup key");
|
||||
|
||||
@@ -121,8 +175,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/email/password-reset",
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
url: "/password-reset",
|
||||
schema: {
|
||||
body: z.object({
|
||||
protectedKey: z.string().trim(),
|
||||
@@ -141,9 +194,10 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const token = validateSignUpAuthorization(req.headers.authorization as string, "",false)!
|
||||
await server.services.password.resetPasswordByBackupKey({
|
||||
...req.body,
|
||||
userId: req.permission.id
|
||||
userId: token.userId,
|
||||
});
|
||||
|
||||
return { message: "Successfully updated backup private key" };
|
||||
|
||||
@@ -65,7 +65,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim(),
|
||||
organizationName: z.string().trim(),
|
||||
organizationName: z.string().trim().min(1),
|
||||
providerAuthToken: z.string().trim().optional().nullish(),
|
||||
attributionSource: z.string().trim().optional()
|
||||
}),
|
||||
|
||||
@@ -21,7 +21,7 @@ export const validateProviderAuthToken = (providerToken: string, email: string)
|
||||
if (decodedToken.email !== email) throw new Error("Invalid auth credentials");
|
||||
};
|
||||
|
||||
export const validateSignUpAuthorization = async (token: string, userId: string) => {
|
||||
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
|
||||
const appCfg = getConfig();
|
||||
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [
|
||||
null,
|
||||
@@ -45,6 +45,7 @@ export const validateSignUpAuthorization = async (token: string, userId: string)
|
||||
AUTH_TOKEN_VALUE,
|
||||
appCfg.JWT_AUTH_SECRET
|
||||
) as AuthModeProviderSignUpTokenPayload;
|
||||
if (!validate) return decodedToken;
|
||||
|
||||
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
|
||||
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
||||
|
||||
@@ -10,7 +10,13 @@ import {
|
||||
TOrgMembershipsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, withTransaction } from "@app/lib/knex";
|
||||
import {
|
||||
buildFindFilter,
|
||||
selectAllTableCols,
|
||||
TFindFilter,
|
||||
TFindOpt,
|
||||
withTransaction
|
||||
} from "@app/lib/knex";
|
||||
|
||||
export type TOrgDalFactory = ReturnType<typeof orgDalFactory>;
|
||||
|
||||
@@ -109,7 +115,7 @@ export const orgDalFactory = (db: TDbClient) => {
|
||||
|
||||
// MEMBERSHIP OPERATIONS
|
||||
// --------------------
|
||||
const orgMembershipOrm = ormify(db, TableName.OrgMembership);
|
||||
// const orgMembershipOrm = ormify(db, TableName.OrgMembership);
|
||||
|
||||
const createMembership = async (data: TOrgMembershipsInsert, tx?: Knex) => {
|
||||
try {
|
||||
@@ -160,6 +166,32 @@ export const orgDalFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findMembership = async (
|
||||
filter: TFindFilter<TOrgMemberships>,
|
||||
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.OrgMembership)
|
||||
.where(buildFindFilter(filter))
|
||||
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.OrgMembership),
|
||||
db.ref("email").withSchema(TableName.Users)
|
||||
);
|
||||
if (limit) query.limit(limit);
|
||||
if (offset) query.offset(offset);
|
||||
if (sort) {
|
||||
query.orderBy(
|
||||
sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))
|
||||
);
|
||||
}
|
||||
const res = await query;
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
}
|
||||
};
|
||||
|
||||
return withTransaction(db, {
|
||||
findAllOrgMembers,
|
||||
findOrgById,
|
||||
@@ -167,7 +199,7 @@ export const orgDalFactory = (db: TDbClient) => {
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
findMembership: orgMembershipOrm.find,
|
||||
findMembership,
|
||||
createMembership,
|
||||
updateMembershipById,
|
||||
deleteMembershipById,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrgMembershipStatus, ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { OrgMembershipStatus, ProjectMembershipRole, TableName } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
@@ -142,7 +142,11 @@ export const projectMembershipServiceFactory = ({
|
||||
);
|
||||
const orgMembers = await orgDal.findMembership({
|
||||
orgId: project.orgId,
|
||||
$in: { id: members.map(({ orgMembershipId }) => orgMembershipId) }
|
||||
$in: {
|
||||
[`${TableName.OrgMembership}.id` as "id"]: members.map(
|
||||
({ orgMembershipId }) => orgMembershipId
|
||||
)
|
||||
}
|
||||
});
|
||||
if (orgMembers.length !== members.length)
|
||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
||||
@@ -180,7 +184,7 @@ export const projectMembershipServiceFactory = ({
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical workspace invitation",
|
||||
recipients: orgMembers.map(({ userId }) => userId).filter(Boolean) as string[],
|
||||
recipients: orgMembers.map(({ email }) => email).filter(Boolean) as string[],
|
||||
substitutions: {
|
||||
inviterFirstName: sender.firstName,
|
||||
inviterEmail: sender.email,
|
||||
|
||||
@@ -197,7 +197,10 @@ export const interpolateSecrets = ({
|
||||
return expandSecrets;
|
||||
};
|
||||
|
||||
export const decryptSecretRaw = (secret: TSecrets, key: string) => {
|
||||
export const decryptSecretRaw = (
|
||||
secret: TSecrets & { workspace: string; environment: string },
|
||||
key: string
|
||||
) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
@@ -225,6 +228,8 @@ export const decryptSecretRaw = (secret: TSecrets, key: string) => {
|
||||
|
||||
return {
|
||||
secretKey,
|
||||
workspace: secret.workspace,
|
||||
environment: secret.environment,
|
||||
secretValue,
|
||||
secretComment,
|
||||
version: secret.version,
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
TGetASecretRawDTO,
|
||||
TGetSecretsDTO,
|
||||
TGetSecretsRawDTO,
|
||||
TGetSecretVersionsDTO,
|
||||
TListSecretVersionDTO,
|
||||
TUpdateBulkSecretDTO,
|
||||
TUpdateSecretDTO,
|
||||
@@ -326,7 +327,7 @@ export const secretServiceFactory = ({
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({ secretPath: path, projectId, environment });
|
||||
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
|
||||
return { ...secret[0], tags };
|
||||
return { ...secret[0], environment, workspace: projectId, tags };
|
||||
};
|
||||
|
||||
const updateSecret = async ({
|
||||
@@ -419,7 +420,7 @@ export const secretServiceFactory = ({
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({ secretPath: path, projectId, environment });
|
||||
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
|
||||
return updatedSecret[0];
|
||||
return { ...updatedSecret[0], workspace: projectId, environment };
|
||||
};
|
||||
|
||||
const deleteSecret = async ({
|
||||
@@ -474,7 +475,7 @@ export const secretServiceFactory = ({
|
||||
await secretQueueService.syncSecrets({ secretPath: path, projectId, environment });
|
||||
|
||||
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
|
||||
return deletedSecret[0];
|
||||
return { ...deletedSecret[0], workspace: projectId, environment };
|
||||
};
|
||||
|
||||
const getSecrets = async ({
|
||||
@@ -515,9 +516,12 @@ export const secretServiceFactory = ({
|
||||
secretDal,
|
||||
folderDal
|
||||
});
|
||||
return { secrets, imports: importedSecrets };
|
||||
return {
|
||||
secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })),
|
||||
imports: importedSecrets
|
||||
};
|
||||
}
|
||||
return { secrets };
|
||||
return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) };
|
||||
};
|
||||
|
||||
const getASecret = async ({
|
||||
@@ -572,14 +576,18 @@ export const secretServiceFactory = ({
|
||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) {
|
||||
return importedSecrets[i].secrets[j];
|
||||
return {
|
||||
...importedSecrets[i].secrets[j],
|
||||
workspace: projectId,
|
||||
environment: importedSecrets[i].environment.slug
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!secret) throw new BadRequestError({ message: "Secret not found" });
|
||||
|
||||
return secret;
|
||||
return { ...secret, workspace: projectId, environment };
|
||||
};
|
||||
|
||||
const createManySecret = async ({
|
||||
@@ -813,7 +821,12 @@ export const secretServiceFactory = ({
|
||||
secrets: secrets.map((el) => decryptSecretRaw(el, botKey)),
|
||||
imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
|
||||
...el,
|
||||
secrets: importedSecrets.map((sec) => decryptSecretRaw(sec, botKey))
|
||||
secrets: importedSecrets.map((sec) =>
|
||||
decryptSecretRaw(
|
||||
{ ...sec, environment: el.environment.slug, workspace: projectId },
|
||||
botKey
|
||||
)
|
||||
)
|
||||
}))
|
||||
};
|
||||
};
|
||||
@@ -953,6 +966,36 @@ export const secretServiceFactory = ({
|
||||
return decryptSecretRaw(secret, botKey);
|
||||
};
|
||||
|
||||
const getSecretVersions = async ({
|
||||
actorId,
|
||||
actor,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
secretId
|
||||
}: TGetSecretVersionsDTO) => {
|
||||
const secret = await secretDal.findById(secretId);
|
||||
if (!secret) throw new BadRequestError({ message: "Failed to find secret" });
|
||||
|
||||
const folder = await folderDal.findById(secret.folderId);
|
||||
if (!folder) throw new BadRequestError({ message: "Failed to find secret" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
folder.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
const secretVersions = await secretVersionDal.find(
|
||||
{ secretId },
|
||||
{ offset, limit, sort: [["createdAt", "desc"]] }
|
||||
);
|
||||
return secretVersions;
|
||||
};
|
||||
|
||||
return {
|
||||
createSecret,
|
||||
deleteSecret,
|
||||
@@ -968,6 +1011,7 @@ export const secretServiceFactory = ({
|
||||
updateSecretRaw,
|
||||
deleteSecretRaw,
|
||||
listSecretVersionsBySecretId,
|
||||
getSecretVersions,
|
||||
// external services function
|
||||
fnSecretBulkDelete,
|
||||
fnSecretBulkUpdate,
|
||||
|
||||
@@ -167,6 +167,12 @@ export type TDeleteSecretRawDTO = TProjectPermission & {
|
||||
type: SecretType;
|
||||
};
|
||||
|
||||
export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
secretId: string;
|
||||
};
|
||||
|
||||
export type TFnSecretBulkInsert = {
|
||||
folderId: string;
|
||||
tx?: Knex;
|
||||
|
||||
@@ -74,6 +74,16 @@ services:
|
||||
- NEXT_PUBLIC_ENV=development
|
||||
- INFISICAL_TELEMETRY_ENABLED=false
|
||||
|
||||
smtp-server:
|
||||
container_name: infisical-dev-smtp-server
|
||||
image: lytrax/mailhog:latest # https://github.com/mailhog/MailHog/issues/353#issuecomment-821137362
|
||||
restart: always
|
||||
logging:
|
||||
driver: "none" # disable saving logs
|
||||
ports:
|
||||
- 1025:1025 # SMTP server
|
||||
- 8025:8025 # Web UI
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
driver: local
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
export enum ProjectMembershipRole {
|
||||
Admin = "admin",
|
||||
Member = "member",
|
||||
Custom = "custom",
|
||||
Viewer = "viewer",
|
||||
NoAccess = "no-access"
|
||||
}
|
||||
|
||||
export type TGetProjectRolesDTO = {
|
||||
workspaceId?: string;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,10 @@ const generalPermissionSchema = z
|
||||
export const formSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
slug: z.string().trim(),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
|
||||
permissions: z
|
||||
.object({
|
||||
workspace: z
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUploadWsKey
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
const addMemberFormSchema = z.object({
|
||||
orgMembershipId: z.string().trim()
|
||||
@@ -158,7 +159,6 @@ export const MemberListTab = () => {
|
||||
() => members?.find(({ user: u }) => userId === u?.id)?.role === "owner",
|
||||
[userId, members]
|
||||
);
|
||||
console.log(members);
|
||||
|
||||
const findRoleFromId = useCallback(
|
||||
(roleId: string) => {
|
||||
@@ -171,7 +171,9 @@ export const MemberListTab = () => {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
try {
|
||||
const isCustomRole = !["admin", "member", "viewer"].includes(role);
|
||||
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
|
||||
role as ProjectMembershipRole
|
||||
);
|
||||
|
||||
if (isCustomRole && subscription && !subscription?.rbac) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
|
||||
@@ -28,7 +28,10 @@ const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const;
|
||||
export const formSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
slug: z.string().trim(),
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
|
||||
permissions: z
|
||||
.object({
|
||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||
|
||||
Reference in New Issue
Block a user