mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 22:02:57 +00:00
Compare commits
23 Commits
infisical-
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
9a5329300c | |||
b03c346985 | |||
84efc3de46 | |||
2ff3818ecb | |||
6fbcbc4807 | |||
9048988e2f | |||
98cfd72928 | |||
2293abfc80 | |||
817a783ec2 | |||
9006212ab5 | |||
1627674c2a | |||
bc65bf1238 | |||
3990b6dc49 | |||
a3b8de2e84 | |||
b5bffdbcac | |||
23e40e523a | |||
d1749deff0 | |||
960aceed29 | |||
466dadc611 | |||
cc5ca30057 | |||
b1981df8f0 | |||
086652a89f | |||
6574b6489f |
20
backend/package-lock.json
generated
20
backend/package-lock.json
generated
@ -45,6 +45,7 @@
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"posthog-node": "^2.6.0",
|
||||
"probot": "^12.3.1",
|
||||
@ -11385,6 +11386,17 @@
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-github": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
|
||||
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
|
||||
"dependencies": {
|
||||
"passport-oauth2": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-google-oauth20": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
|
||||
@ -22858,6 +22870,14 @@
|
||||
"utils-merge": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"passport-github": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
|
||||
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
|
||||
"requires": {
|
||||
"passport-oauth2": "1.x.x"
|
||||
}
|
||||
},
|
||||
"passport-google-oauth20": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
|
||||
|
@ -36,6 +36,7 @@
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"posthog-node": "^2.6.0",
|
||||
"probot": "^12.3.1",
|
||||
|
@ -36,7 +36,6 @@ export const getClientIdVercel = async () => (await client.getSecret("CLIENT_ID_
|
||||
export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
|
||||
export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
|
||||
export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
|
||||
export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue;
|
||||
export const getClientIdBitBucket = async () => (await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
|
||||
export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
|
||||
export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
|
||||
@ -44,9 +43,14 @@ export const getClientSecretVercel = async () => (await client.getSecret("CLIENT
|
||||
export const getClientSecretNetlify = async () => (await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
|
||||
export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
|
||||
export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
|
||||
export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue;
|
||||
export const getClientSecretBitBucket = async () => (await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
|
||||
export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
|
||||
|
||||
export const getClientIdGoogleLogin = async () => (await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
|
||||
export const getClientSecretGoogleLogin = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
|
||||
export const getClientIdGitHubLogin = async () => (await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
|
||||
export const getClientSecretGitHubLogin = async () => (await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
|
||||
|
||||
export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
|
||||
export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
|
||||
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;
|
||||
|
@ -123,9 +123,15 @@ export const updateAuthProvider = async (req: Request, res: Response) => {
|
||||
authProvider
|
||||
} = req.body;
|
||||
|
||||
if (req.user?.authProvider === AuthProvider.OKTA_SAML) return res.status(400).send({
|
||||
message: "Failed to update user authentication method because SAML SSO is enforced"
|
||||
});
|
||||
if (
|
||||
req.user?.authProvider === AuthProvider.OKTA_SAML
|
||||
|| req.user?.authProvider === AuthProvider.AZURE_SAML
|
||||
|| req.user?.authProvider === AuthProvider.JUMPCLOUD_SAML
|
||||
) {
|
||||
return res.status(400).send({
|
||||
message: "Failed to update user authentication method because SAML SSO is enforced"
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
req.user._id.toString(),
|
||||
|
@ -137,6 +137,12 @@ export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete payment method with id [pmtMethodId] for organization
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||
const { pmtMethodId } = req.params;
|
||||
|
||||
@ -206,4 +212,18 @@ export const getOrganizationInvoices = async (req: Request, res: Response) => {
|
||||
);
|
||||
|
||||
return res.status(200).send(invoices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return organization's licenses on file
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationLicenses = async (req: Request, res: Response) => {
|
||||
const { data: { licenses } } = await licenseServerKeyRequest.get(
|
||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/licenses`
|
||||
);
|
||||
|
||||
return res.status(200).send(licenses);
|
||||
}
|
@ -220,4 +220,18 @@ router.get(
|
||||
organizationsController.getOrganizationInvoices
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:organizationId/licenses",
|
||||
requireAuth({
|
||||
acceptedAuthModes: ["jwt"],
|
||||
}),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
param("organizationId").exists().trim(),
|
||||
validateRequest,
|
||||
organizationsController.getOrganizationLicenses
|
||||
);
|
||||
|
||||
export default router;
|
@ -41,6 +41,29 @@ router.get(
|
||||
ssoController.redirectSSO
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/redirect/github",
|
||||
authLimiter,
|
||||
(req, res, next) => {
|
||||
passport.authenticate("github", {
|
||||
session: false,
|
||||
...(req.query.callback_port ? {
|
||||
state: req.query.callback_port as string
|
||||
} : {})
|
||||
})(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/github",
|
||||
authLimiter,
|
||||
passport.authenticate("github", {
|
||||
failureRedirect: "/login/provider/error",
|
||||
session: false
|
||||
}),
|
||||
ssoController.redirectSSO
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/redirect/saml2/:ssoIdentifier",
|
||||
authLimiter,
|
||||
|
@ -4,18 +4,20 @@ import {
|
||||
decryptAsymmetric,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
generateKeyPair,
|
||||
generateKeyPair
|
||||
} from "../utils/crypto";
|
||||
import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../variables";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
||||
import { InternalServerError } from "../utils/errors";
|
||||
import Folder from "../models/folder";
|
||||
import { getFolderByPath } from "../services/FolderService";
|
||||
import { getAllImportedSecrets } from "../services/SecretImportService";
|
||||
import { expandSecrets } from "./secrets";
|
||||
|
||||
/**
|
||||
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||
@ -25,7 +27,7 @@ import { getFolderByPath } from "../services/FolderService";
|
||||
*/
|
||||
export const createBot = async ({
|
||||
name,
|
||||
workspaceId,
|
||||
workspaceId
|
||||
}: {
|
||||
name: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
@ -36,10 +38,7 @@ export const createBot = async ({
|
||||
const { publicKey, privateKey } = generateKeyPair();
|
||||
|
||||
if (rootEncryptionKey) {
|
||||
const { ciphertext, iv, tag } = client.encryptSymmetric(
|
||||
privateKey,
|
||||
rootEncryptionKey
|
||||
);
|
||||
const { ciphertext, iv, tag } = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||
|
||||
return await new Bot({
|
||||
name,
|
||||
@ -50,12 +49,12 @@ export const createBot = async ({
|
||||
iv,
|
||||
tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64,
|
||||
keyEncoding: ENCODING_SCHEME_BASE64
|
||||
}).save();
|
||||
} else if (encryptionKey) {
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext: privateKey,
|
||||
key: await getEncryptionKey(),
|
||||
key: await getEncryptionKey()
|
||||
});
|
||||
|
||||
return await new Bot({
|
||||
@ -67,12 +66,12 @@ export const createBot = async ({
|
||||
iv,
|
||||
tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
}).save();
|
||||
}
|
||||
|
||||
throw InternalServerError({
|
||||
message: "Failed to create new bot due to missing encryption key",
|
||||
message: "Failed to create new bot due to missing encryption key"
|
||||
});
|
||||
};
|
||||
|
||||
@ -82,7 +81,7 @@ export const createBot = async ({
|
||||
*/
|
||||
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
|
||||
const botKey = await BotKey.exists({
|
||||
workspace: workspaceId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
return botKey ? false : true;
|
||||
@ -98,19 +97,19 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
|
||||
export const getSecretsBotHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretPath
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}) => {
|
||||
const content = {} as any;
|
||||
const content: Record<string, { value: string; comment?: string }> = {};
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
environment
|
||||
});
|
||||
|
||||
if (!folders && secretPath !== "/") {
|
||||
@ -129,7 +128,43 @@ export const getSecretsBotHelper = async ({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
type: SECRET_SHARED,
|
||||
folder: folderId,
|
||||
folder: folderId
|
||||
});
|
||||
|
||||
const importedSecrets = await getAllImportedSecrets(
|
||||
workspaceId.toString(),
|
||||
environment,
|
||||
folderId
|
||||
);
|
||||
|
||||
importedSecrets.forEach(({ secrets }) => {
|
||||
secrets.forEach((secret) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
content[secretKey] = { value: secretValue };
|
||||
|
||||
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
||||
const commentValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretCommentCiphertext,
|
||||
iv: secret.secretCommentIV,
|
||||
tag: secret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
content[secretKey].comment = commentValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
@ -137,19 +172,31 @@ export const getSecretsBotHelper = async ({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
content[secretKey] = secretValue;
|
||||
content[secretKey] = { value: secretValue };
|
||||
|
||||
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
||||
const commentValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretCommentCiphertext,
|
||||
iv: secret.secretCommentIV,
|
||||
tag: secret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
content[secretKey].comment = commentValue;
|
||||
}
|
||||
});
|
||||
|
||||
await expandSecrets(workspaceId.toString(), key, content);
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
@ -160,22 +207,18 @@ export const getSecretsBotHelper = async ({
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @returns {String} key - decrypted workspace key
|
||||
*/
|
||||
export const getKey = async ({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) => {
|
||||
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
|
||||
const encryptionKey = await getEncryptionKey();
|
||||
const rootEncryptionKey = await getRootEncryptionKey();
|
||||
|
||||
const botKey = await BotKey.findOne({
|
||||
workspace: workspaceId,
|
||||
workspace: workspaceId
|
||||
}).populate<{ sender: IUser }>("sender", "publicKey");
|
||||
|
||||
if (!botKey) throw new Error("Failed to find bot key");
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
workspace: workspaceId
|
||||
}).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
|
||||
|
||||
if (!bot) throw new Error("Failed to find bot");
|
||||
@ -194,7 +237,7 @@ export const getKey = async ({
|
||||
ciphertext: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
publicKey: botKey.sender.publicKey as string,
|
||||
privateKey: privateKeyBot,
|
||||
privateKey: privateKeyBot
|
||||
});
|
||||
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||
// case: encoding scheme is utf8
|
||||
@ -202,20 +245,19 @@ export const getKey = async ({
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
key: encryptionKey,
|
||||
key: encryptionKey
|
||||
});
|
||||
|
||||
return decryptAsymmetric({
|
||||
ciphertext: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
publicKey: botKey.sender.publicKey as string,
|
||||
privateKey: privateKeyBot,
|
||||
privateKey: privateKeyBot
|
||||
});
|
||||
}
|
||||
|
||||
throw InternalServerError({
|
||||
message:
|
||||
"Failed to obtain bot's copy of workspace key needed for bot operations",
|
||||
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
|
||||
});
|
||||
};
|
||||
|
||||
@ -228,7 +270,7 @@ export const getKey = async ({
|
||||
*/
|
||||
export const encryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
plaintext,
|
||||
plaintext
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
plaintext: string;
|
||||
@ -236,13 +278,13 @@ export const encryptSymmetricHelper = async ({
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||
plaintext,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
return {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
tag
|
||||
};
|
||||
};
|
||||
/**
|
||||
@ -258,7 +300,7 @@ export const decryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
tag
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
ciphertext: string;
|
||||
@ -270,7 +312,7 @@ export const decryptSymmetricHelper = async ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
return plaintext;
|
||||
@ -281,24 +323,24 @@ export const decryptSymmetricHelper = async ({
|
||||
* and [envionment] using bot
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - environment
|
||||
* @param {String} obj.environment - environment
|
||||
*/
|
||||
export const getSecretsCommentBotHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
} : {
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}) => {
|
||||
const content = {} as any;
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
|
||||
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
environment
|
||||
});
|
||||
|
||||
if (!folders && secretPath !== "/") {
|
||||
@ -317,23 +359,23 @@ export const getSecretsCommentBotHelper = async ({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
type: SECRET_SHARED,
|
||||
folder: folderId,
|
||||
folder: folderId
|
||||
});
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
if(secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
||||
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
|
||||
const commentValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretCommentCiphertext,
|
||||
iv: secret.secretCommentIV,
|
||||
tag: secret.secretCommentTag,
|
||||
key,
|
||||
key
|
||||
});
|
||||
|
||||
content[secretKey] = commentValue;
|
||||
@ -341,4 +383,4 @@ export const getSecretsCommentBotHelper = async ({
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_VERCEL
|
||||
} from "../variables";
|
||||
import { UnauthorizedRequestError } from "../utils/errors";
|
||||
import * as Sentry from "@sentry/node";
|
||||
@ -34,7 +34,7 @@ export const handleOAuthExchangeHelper = async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
code,
|
||||
environment,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
@ -43,21 +43,20 @@ export const handleOAuthExchangeHelper = async ({
|
||||
}) => {
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot)
|
||||
throw new Error("Bot must be enabled for OAuth2 code-token exchange");
|
||||
if (!bot) throw new Error("Bot must be enabled for OAuth2 code-token exchange");
|
||||
|
||||
// exchange code for access and refresh tokens
|
||||
const res = await exchangeCode({
|
||||
integration,
|
||||
code,
|
||||
code
|
||||
});
|
||||
|
||||
const update: Update = {
|
||||
workspace: workspaceId,
|
||||
integration,
|
||||
integration
|
||||
};
|
||||
|
||||
switch (integration) {
|
||||
@ -72,12 +71,12 @@ export const handleOAuthExchangeHelper = async ({
|
||||
const integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
integration,
|
||||
integration
|
||||
},
|
||||
update,
|
||||
{
|
||||
new: true,
|
||||
upsert: true,
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
|
||||
@ -86,7 +85,7 @@ export const handleOAuthExchangeHelper = async ({
|
||||
// set integration auth refresh token
|
||||
await setIntegrationAuthRefreshHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
refreshToken: res.refreshToken,
|
||||
refreshToken: res.refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
@ -97,7 +96,7 @@ export const handleOAuthExchangeHelper = async ({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessId: null,
|
||||
accessToken: res.accessToken,
|
||||
accessExpiresAt: res.accessExpiresAt,
|
||||
accessExpiresAt: res.accessExpiresAt
|
||||
});
|
||||
}
|
||||
|
||||
@ -111,7 +110,7 @@ export const handleOAuthExchangeHelper = async ({
|
||||
*/
|
||||
export const syncIntegrationsHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment?: string;
|
||||
@ -121,11 +120,11 @@ export const syncIntegrationsHelper = async ({
|
||||
workspace: workspaceId,
|
||||
...(environment
|
||||
? {
|
||||
environment,
|
||||
}
|
||||
: {}),
|
||||
environment
|
||||
}
|
||||
: {}),
|
||||
isActive: true,
|
||||
app: { $ne: null },
|
||||
app: { $ne: null }
|
||||
});
|
||||
|
||||
// for each workspace integration, sync/push secrets
|
||||
@ -135,25 +134,16 @@ export const syncIntegrationsHelper = async ({
|
||||
const secrets = await BotService.getSecrets({
|
||||
workspaceId: integration.workspace,
|
||||
environment: integration.environment,
|
||||
secretPath: integration.secretPath,
|
||||
secretPath: integration.secretPath
|
||||
});
|
||||
|
||||
// get workspace, environment (shared) secrets comments
|
||||
const secretComments = await BotService.getSecretComments({
|
||||
workspaceId: integration.workspace,
|
||||
environment: integration.environment,
|
||||
secretPath: integration.secretPath,
|
||||
})
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(
|
||||
integration.integrationAuth
|
||||
);
|
||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||
|
||||
if (!integrationAuth) throw new Error("Failed to find integration auth");
|
||||
|
||||
|
||||
// get integration auth access token
|
||||
const access = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integration.integrationAuth,
|
||||
integrationAuthId: integration.integrationAuth
|
||||
});
|
||||
|
||||
// sync secrets to integration
|
||||
@ -162,14 +152,17 @@ export const syncIntegrationsHelper = async ({
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessId: access.accessId === undefined ? null : access.accessId,
|
||||
accessToken: access.accessToken,
|
||||
secretComments
|
||||
accessToken: access.accessToken
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
console.log(`syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`, err) // eslint-disable-line no-use-before-define
|
||||
throw err
|
||||
// eslint-disable-next-line
|
||||
console.log(
|
||||
`syncIntegrationsHelper: failed with [workspaceId=${workspaceId}] [environment=${environment}]`,
|
||||
err
|
||||
); // eslint-disable-line no-use-before-define
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
@ -182,24 +175,24 @@ export const syncIntegrationsHelper = async ({
|
||||
* @param {String} refreshToken - decrypted refresh token
|
||||
*/
|
||||
export const getIntegrationAuthRefreshHelper = async ({
|
||||
integrationAuthId,
|
||||
integrationAuthId
|
||||
}: {
|
||||
integrationAuthId: Types.ObjectId;
|
||||
}) => {
|
||||
const integrationAuth = await IntegrationAuth.findById(
|
||||
integrationAuthId
|
||||
).select("+refreshCiphertext +refreshIV +refreshTag");
|
||||
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
|
||||
"+refreshCiphertext +refreshIV +refreshTag"
|
||||
);
|
||||
|
||||
if (!integrationAuth)
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to locate Integration Authentication credentials",
|
||||
message: "Failed to locate Integration Authentication credentials"
|
||||
});
|
||||
|
||||
const refreshToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||
iv: integrationAuth.refreshIV as string,
|
||||
tag: integrationAuth.refreshTag as string,
|
||||
tag: integrationAuth.refreshTag as string
|
||||
});
|
||||
|
||||
return refreshToken;
|
||||
@ -214,28 +207,26 @@ export const getIntegrationAuthRefreshHelper = async ({
|
||||
* @returns {String} accessToken - decrypted access token
|
||||
*/
|
||||
export const getIntegrationAuthAccessHelper = async ({
|
||||
integrationAuthId,
|
||||
integrationAuthId
|
||||
}: {
|
||||
integrationAuthId: Types.ObjectId;
|
||||
}) => {
|
||||
let accessId;
|
||||
let accessToken;
|
||||
const integrationAuth = await IntegrationAuth.findById(
|
||||
integrationAuthId
|
||||
).select(
|
||||
const integrationAuth = await IntegrationAuth.findById(integrationAuthId).select(
|
||||
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
|
||||
);
|
||||
|
||||
if (!integrationAuth)
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to locate Integration Authentication credentials",
|
||||
message: "Failed to locate Integration Authentication credentials"
|
||||
});
|
||||
|
||||
accessToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.accessCiphertext as string,
|
||||
iv: integrationAuth.accessIV as string,
|
||||
tag: integrationAuth.accessTag as string,
|
||||
tag: integrationAuth.accessTag as string
|
||||
});
|
||||
|
||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||
@ -245,11 +236,11 @@ export const getIntegrationAuthAccessHelper = async ({
|
||||
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||
// access token is expired
|
||||
const refreshToken = await getIntegrationAuthRefreshHelper({
|
||||
integrationAuthId,
|
||||
integrationAuthId
|
||||
});
|
||||
accessToken = await exchangeRefresh({
|
||||
integrationAuth,
|
||||
refreshToken,
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -263,13 +254,13 @@ export const getIntegrationAuthAccessHelper = async ({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
ciphertext: integrationAuth.accessIdCiphertext as string,
|
||||
iv: integrationAuth.accessIdIV as string,
|
||||
tag: integrationAuth.accessIdTag as string,
|
||||
tag: integrationAuth.accessIdTag as string
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
accessId,
|
||||
accessToken,
|
||||
accessToken
|
||||
};
|
||||
};
|
||||
|
||||
@ -283,7 +274,7 @@ export const getIntegrationAuthAccessHelper = async ({
|
||||
*/
|
||||
export const setIntegrationAuthRefreshHelper = async ({
|
||||
integrationAuthId,
|
||||
refreshToken,
|
||||
refreshToken
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
refreshToken: string;
|
||||
@ -294,22 +285,22 @@ export const setIntegrationAuthRefreshHelper = async ({
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: refreshToken,
|
||||
plaintext: refreshToken
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
||||
{
|
||||
_id: integrationAuthId,
|
||||
_id: integrationAuthId
|
||||
},
|
||||
{
|
||||
refreshCiphertext: obj.ciphertext,
|
||||
refreshIV: obj.iv,
|
||||
refreshTag: obj.tag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
@ -329,7 +320,7 @@ export const setIntegrationAuthAccessHelper = async ({
|
||||
integrationAuthId,
|
||||
accessId,
|
||||
accessToken,
|
||||
accessExpiresAt,
|
||||
accessExpiresAt
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessId: string | null;
|
||||
@ -342,20 +333,20 @@ export const setIntegrationAuthAccessHelper = async ({
|
||||
|
||||
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: accessToken,
|
||||
plaintext: accessToken
|
||||
});
|
||||
|
||||
let encryptedAccessIdObj;
|
||||
if (accessId) {
|
||||
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace,
|
||||
plaintext: accessId,
|
||||
plaintext: accessId
|
||||
});
|
||||
}
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
||||
{
|
||||
_id: integrationAuthId,
|
||||
_id: integrationAuthId
|
||||
},
|
||||
{
|
||||
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
||||
@ -366,10 +357,10 @@ export const setIntegrationAuthAccessHelper = async ({
|
||||
accessTag: encryptedAccessTokenObj.tag,
|
||||
accessExpiresAt,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -42,9 +42,10 @@ import { TelemetryService } from "../services";
|
||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
||||
import { EELogService, EESecretService } from "../ee/services";
|
||||
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
|
||||
import { getFolderIdFromServiceToken } from "../services/FolderService";
|
||||
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
|
||||
import picomatch from "picomatch";
|
||||
import path from "path";
|
||||
import Folder, { TFolderRootSchema } from "../models/folder";
|
||||
|
||||
export const isValidScope = (
|
||||
authPayload: IServiceTokenData,
|
||||
@ -64,10 +65,9 @@ export const isValidScope = (
|
||||
export function containsGlobPatterns(secretPath: string) {
|
||||
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
|
||||
const normalizedPath = path.normalize(secretPath);
|
||||
return globChars.some(char => normalizedPath.includes(char));
|
||||
return globChars.some((char) => normalizedPath.includes(char));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
||||
*
|
||||
@ -929,3 +929,164 @@ export const deleteSecretHelper = async ({
|
||||
secret
|
||||
};
|
||||
};
|
||||
|
||||
const fetchSecretsCrossEnv = (workspaceId: string, folders: TFolderRootSchema[], key: string) => {
|
||||
const fetchCache: Record<string, Record<string, string>> = {};
|
||||
|
||||
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
|
||||
const secRefPathUrl = path.join("/", ...secRefPath);
|
||||
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
|
||||
|
||||
if (fetchCache?.[uniqKey]) {
|
||||
return fetchCache[uniqKey][secRefKey];
|
||||
}
|
||||
|
||||
let folderId = "root";
|
||||
const folder = folders.find(({ environment }) => environment === secRefEnv);
|
||||
if (!folder && secRefPathUrl !== "/") {
|
||||
throw BadRequestError({ message: "Folder not found" });
|
||||
}
|
||||
|
||||
if (folder) {
|
||||
const selectedFolder = getFolderByPath(folder.nodes, secRefPathUrl);
|
||||
if (!selectedFolder) {
|
||||
throw BadRequestError({ message: "Folder not found" });
|
||||
}
|
||||
folderId = selectedFolder.id;
|
||||
}
|
||||
|
||||
const secrets = await Secret.find({
|
||||
workspace: workspaceId,
|
||||
environment: secRefEnv,
|
||||
type: SECRET_SHARED,
|
||||
folder: folderId
|
||||
});
|
||||
|
||||
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
prev[secretKey] = secretValue;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
fetchCache[uniqKey] = decryptedSec;
|
||||
|
||||
return fetchCache[uniqKey][secRefKey];
|
||||
};
|
||||
};
|
||||
|
||||
const INTERPOLATION_SYNTAX_REG = new RegExp(/\${([^}]+)}/g);
|
||||
const recursivelyExpandSecret = async (
|
||||
expandedSec: Record<string, string>,
|
||||
interpolatedSec: Record<string, string>,
|
||||
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
|
||||
recursionChainBreaker: Record<string, boolean>,
|
||||
key: string
|
||||
) => {
|
||||
if (expandedSec?.[key]) {
|
||||
return expandedSec[key];
|
||||
}
|
||||
if (recursionChainBreaker?.[key]) {
|
||||
return "";
|
||||
}
|
||||
recursionChainBreaker[key] = true;
|
||||
|
||||
let interpolatedValue = interpolatedSec[key];
|
||||
if (!interpolatedValue) {
|
||||
throw new Error(`Couldn't find referenced value - ${key}`);
|
||||
}
|
||||
|
||||
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
|
||||
if (refs) {
|
||||
for (const interpolationSyntax of refs) {
|
||||
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
|
||||
const entities = interpolationKey.trim().split(".");
|
||||
|
||||
if (entities.length === 1) {
|
||||
const val = await recursivelyExpandSecret(
|
||||
expandedSec,
|
||||
interpolatedSec,
|
||||
fetchCrossEnv,
|
||||
recursionChainBreaker,
|
||||
interpolationKey
|
||||
);
|
||||
if (val) {
|
||||
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entities.length > 1) {
|
||||
const secRefEnv = entities[0];
|
||||
const secRefPath = entities.slice(1, entities.length - 1);
|
||||
const secRefKey = entities[entities.length - 1];
|
||||
|
||||
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
|
||||
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expandedSec[key] = interpolatedValue;
|
||||
return interpolatedValue;
|
||||
};
|
||||
|
||||
// used to convert multi line ones to quotes ones with \n
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
};
|
||||
|
||||
export const expandSecrets = async (
|
||||
workspaceId: string,
|
||||
rootEncKey: string,
|
||||
secrets: Record<string, { value: string; comment?: string }>
|
||||
) => {
|
||||
const expandedSec: Record<string, string> = {};
|
||||
const interpolatedSec: Record<string, string> = {};
|
||||
|
||||
const folders = await Folder.find({ workspace: workspaceId });
|
||||
const crossSecEnvFetch = fetchSecretsCrossEnv(workspaceId, folders, rootEncKey);
|
||||
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
|
||||
interpolatedSec[key] = secrets[key].value;
|
||||
} else {
|
||||
expandedSec[key] = secrets[key].value;
|
||||
}
|
||||
});
|
||||
|
||||
for (const key of Object.keys(secrets)) {
|
||||
if (expandedSec?.[key]) {
|
||||
secrets[key].value = formatMultiValueEnv(expandedSec[key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
|
||||
// so for any recursion building if there is an entity two times same key meaning it will be looped
|
||||
const recursionChainBreaker: Record<string, boolean> = {};
|
||||
const expandedVal = await recursivelyExpandSecret(
|
||||
expandedSec,
|
||||
interpolatedSec,
|
||||
crossSecEnvFetch,
|
||||
recursionChainBreaker,
|
||||
key
|
||||
);
|
||||
|
||||
secrets[key].value = formatMultiValueEnv(expandedVal);
|
||||
}
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ import { Document, Schema, Types, model } from "mongoose";
|
||||
export enum AuthProvider {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
GITHUB = "github",
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml",
|
||||
JUMPCLOUD_SAML = "jumpcloud-saml",
|
||||
|
@ -50,7 +50,8 @@ router.patch(
|
||||
}),
|
||||
body("authProvider").exists().isString().isIn([
|
||||
AuthProvider.EMAIL,
|
||||
AuthProvider.GOOGLE
|
||||
AuthProvider.GOOGLE,
|
||||
AuthProvider.GITHUB
|
||||
]),
|
||||
validateRequest,
|
||||
usersController.updateAuthProvider
|
||||
|
@ -57,7 +57,7 @@ router.get(
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.getSecretByNameRaw
|
||||
);
|
||||
@ -86,7 +86,7 @@ router.post(
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.createSecretRaw
|
||||
);
|
||||
@ -115,7 +115,7 @@ router.patch(
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.updateSecretByNameRaw
|
||||
);
|
||||
@ -143,7 +143,7 @@ router.delete(
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: true,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.deleteSecretByNameRaw
|
||||
);
|
||||
@ -169,7 +169,7 @@ router.get(
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.getSecrets
|
||||
);
|
||||
@ -205,7 +205,7 @@ router.post(
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.createSecret
|
||||
);
|
||||
@ -232,7 +232,7 @@ router.get(
|
||||
locationEnvironment: "query",
|
||||
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.getSecretByName
|
||||
);
|
||||
@ -263,7 +263,7 @@ router.patch(
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.updateSecretByName
|
||||
);
|
||||
@ -291,7 +291,7 @@ router.delete(
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||
requireBlindIndicesEnabled: true,
|
||||
requireE2EEOff: false,
|
||||
checkIPAllowlist: false
|
||||
checkIPAllowlist: true
|
||||
}),
|
||||
secretsController.deleteSecretByName
|
||||
);
|
||||
|
@ -12,8 +12,10 @@ import {
|
||||
} from "../models";
|
||||
import { createToken } from "../helpers/auth";
|
||||
import {
|
||||
getClientIdGoogle,
|
||||
getClientSecretGoogle,
|
||||
getClientIdGitHubLogin,
|
||||
getClientIdGoogleLogin,
|
||||
getClientSecretGitHubLogin,
|
||||
getClientSecretGoogleLogin,
|
||||
getJwtProviderAuthLifetime,
|
||||
getJwtProviderAuthSecret,
|
||||
} from "../config";
|
||||
@ -25,6 +27,8 @@ import { getSiteURL } from "../config";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const GitHubStrategy = require("passport-github").Strategy;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
|
||||
|
||||
/**
|
||||
@ -67,42 +71,97 @@ const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
||||
}
|
||||
|
||||
const initializePassport = async () => {
|
||||
const googleClientSecret = await getClientSecretGoogle();
|
||||
const googleClientId = await getClientIdGoogle();
|
||||
const clientIdGoogleLogin = await getClientIdGoogleLogin();
|
||||
const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
|
||||
const clientIdGitHubLogin = await getClientIdGitHubLogin();
|
||||
const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
|
||||
|
||||
passport.use(new GoogleStrategy({
|
||||
passReqToCallback: true,
|
||||
clientID: googleClientId,
|
||||
clientSecret: googleClientSecret,
|
||||
callbackURL: "/api/v1/sso/google",
|
||||
scope: ["profile", " email"],
|
||||
}, async (
|
||||
req: express.Request,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: any,
|
||||
done: any
|
||||
) => {
|
||||
try {
|
||||
if (clientIdGoogleLogin && clientSecretGoogleLogin) {
|
||||
passport.use(new GoogleStrategy({
|
||||
passReqToCallback: true,
|
||||
clientID: clientIdGoogleLogin,
|
||||
clientSecret: clientSecretGoogleLogin,
|
||||
callbackURL: "/api/v1/sso/google",
|
||||
scope: ["profile", " email"],
|
||||
}, async (
|
||||
req: express.Request,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: any,
|
||||
done: any
|
||||
) => {
|
||||
try {
|
||||
const email = profile.emails[0].value;
|
||||
|
||||
let user = await User.findOne({
|
||||
email
|
||||
}).select("+publicKey");
|
||||
|
||||
if (user && user.authProvider !== AuthProvider.GOOGLE) {
|
||||
done(InternalServerError());
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email,
|
||||
authProvider: AuthProvider.GOOGLE,
|
||||
authId: profile.id,
|
||||
firstName: profile.name.givenName,
|
||||
lastName: profile.name.familyName
|
||||
}).save();
|
||||
}
|
||||
|
||||
const isUserCompleted = !!user.publicKey;
|
||||
const providerAuthToken = createToken({
|
||||
payload: {
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
authProvider: user.authProvider,
|
||||
isUserCompleted,
|
||||
...(req.query.state ? {
|
||||
callbackPort: req.query.state as string
|
||||
} : {})
|
||||
},
|
||||
expiresIn: await getJwtProviderAuthLifetime(),
|
||||
secret: await getJwtProviderAuthSecret(),
|
||||
});
|
||||
|
||||
req.isUserCompleted = isUserCompleted;
|
||||
req.providerAuthToken = providerAuthToken;
|
||||
done(null, profile);
|
||||
} catch (err) {
|
||||
done(null, false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (clientIdGitHubLogin && clientSecretGitHubLogin) {
|
||||
passport.use(new GitHubStrategy({
|
||||
passReqToCallback: true,
|
||||
clientID: clientIdGitHubLogin,
|
||||
clientSecret: clientSecretGitHubLogin,
|
||||
callbackURL: "/api/v1/sso/github"
|
||||
},
|
||||
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
|
||||
const email = profile.emails[0].value;
|
||||
const firstName = profile.name.givenName;
|
||||
const lastName = profile.name.familyName;
|
||||
|
||||
let user = await User.findOne({
|
||||
email
|
||||
}).select("+publicKey");
|
||||
|
||||
if (user && user.authProvider !== AuthProvider.GOOGLE) {
|
||||
if (user && user.authProvider !== AuthProvider.GITHUB) {
|
||||
done(InternalServerError());
|
||||
}
|
||||
|
||||
|
||||
if (!user) {
|
||||
user = await new User({
|
||||
email,
|
||||
authProvider: AuthProvider.GOOGLE,
|
||||
email: email,
|
||||
authProvider: AuthProvider.GITHUB,
|
||||
authId: profile.id,
|
||||
firstName,
|
||||
lastName
|
||||
firstName: profile.displayName,
|
||||
lastName: ""
|
||||
}).save();
|
||||
}
|
||||
|
||||
@ -111,8 +170,8 @@ const initializePassport = async () => {
|
||||
payload: {
|
||||
userId: user._id.toString(),
|
||||
email: user.email,
|
||||
firstName,
|
||||
lastName,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
authProvider: user.authProvider,
|
||||
isUserCompleted,
|
||||
...(req.query.state ? {
|
||||
@ -125,11 +184,10 @@ const initializePassport = async () => {
|
||||
|
||||
req.isUserCompleted = isUserCompleted;
|
||||
req.providerAuthToken = providerAuthToken;
|
||||
done(null, profile);
|
||||
} catch (err) {
|
||||
done(null, false);
|
||||
return done(null, profile);
|
||||
}
|
||||
}));
|
||||
));
|
||||
}
|
||||
|
||||
passport.use("saml", new MultiSamlStrategy(
|
||||
{
|
||||
|
@ -24,8 +24,6 @@ import {
|
||||
reencryptSecretBlindIndexDataSalts
|
||||
} from "./reencryptData";
|
||||
import {
|
||||
getClientIdGoogle,
|
||||
getClientSecretGoogle,
|
||||
getMongoURL,
|
||||
getNodeEnv,
|
||||
getSentryDSN
|
||||
@ -55,12 +53,7 @@ export const setup = async () => {
|
||||
// initializing the database connection
|
||||
await DatabaseService.initDatabase(await getMongoURL());
|
||||
|
||||
const googleClientSecret: string = await getClientSecretGoogle();
|
||||
const googleClientId: string = await getClientIdGoogle();
|
||||
|
||||
if (googleClientId && googleClientSecret) {
|
||||
await initializePassport();
|
||||
}
|
||||
await initializePassport();
|
||||
|
||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||
|
@ -1,9 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"lib": ["es6", "es2021"],
|
||||
"module": "commonjs",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true,
|
||||
@ -15,15 +13,8 @@
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"skipLibCheck": true,
|
||||
"typeRoots": [
|
||||
"./src/types",
|
||||
"./node_modules/@types"
|
||||
]
|
||||
"typeRoots": ["./src/types", "./node_modules/@types"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
@ -23,7 +23,8 @@
|
||||
},
|
||||
"feedback": {
|
||||
"suggestEdit": true,
|
||||
"raiseIssue": true
|
||||
"raiseIssue": true,
|
||||
"thumbsRating": true
|
||||
},
|
||||
"api": {
|
||||
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"],
|
||||
|
@ -5,7 +5,7 @@ description: "Configure your environment variables when self-hosting Infisical."
|
||||
|
||||
## Backend environment variables
|
||||
|
||||
Depending on your choosen self hosted deployment method, you may need to configured at least the required environment variable listed below.
|
||||
Depending on your choosen self hosted deployment method, you may need to configured at least the required environment variable listed below.
|
||||
Other environment variables are listed below to increase the functionality of your self hosted instance based on your use case.
|
||||
|
||||
<Tabs>
|
||||
@ -14,25 +14,40 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="JWT_SIGNUP_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
|
||||
</ParamField>
|
||||
{" "}
|
||||
|
||||
<ParamField query="JWT_REFRESH_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
|
||||
</ParamField>
|
||||
<ParamField query="JWT_SIGNUP_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
|
||||
16`
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="JWT_AUTH_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
|
||||
</ParamField>
|
||||
{" "}
|
||||
|
||||
<ParamField query="JWT_MFA_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
|
||||
</ParamField>
|
||||
<ParamField query="JWT_REFRESH_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
|
||||
16`
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="JWT_SERVICE_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
|
||||
</ParamField>
|
||||
{" "}
|
||||
|
||||
<ParamField query="JWT_AUTH_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
|
||||
16`
|
||||
</ParamField>
|
||||
|
||||
{" "}
|
||||
|
||||
<ParamField query="JWT_MFA_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
|
||||
16`
|
||||
</ParamField>
|
||||
|
||||
{" "}
|
||||
|
||||
<ParamField query="JWT_SERVICE_SECRET" type="string" default="none" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex
|
||||
16`
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="MONGO_URL" type="string" default="none" required>
|
||||
*TLS based connection string is not yet supported
|
||||
@ -58,7 +73,7 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SMTP_SECURE" type="string" default="none" optional>
|
||||
If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
|
||||
If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
|
||||
@ -68,9 +83,10 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
|
||||
Name label to be used in From field (e.g. Team)
|
||||
</ParamField>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Secret Integrations">
|
||||
To sync secret to third party services, provide value for the related services
|
||||
To sync secret to third party services, provide value for the related services
|
||||
|
||||
<ParamField query="CLIENT_ID_HEROKU" type="string" default="none" optional>
|
||||
OAuth2 client ID for Heroku integration
|
||||
@ -81,7 +97,7 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CLIENT_ID_VERCEL" type="string" default="none" optional>
|
||||
OAuth2 client ID for Vercel integration
|
||||
OAuth2 client ID for Vercel integration
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CLIENT_SECRET_VERCEL" type="string" default="none" optional>
|
||||
@ -89,7 +105,7 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CLIENT_ID_NETLIFY" type="string" default="none" optional>
|
||||
OAuth2 client ID for Netlify integration
|
||||
OAuth2 client ID for Netlify integration
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CLIENT_SECRET_NETLIFY" type="string" default="none" optional>
|
||||
@ -97,7 +113,7 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CLIENT_ID_GITHUB" type="string" default="none" optional>
|
||||
OAuth2 client ID for GitHub integration
|
||||
OAuth2 client ID for GitHub integration
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CLIENT_SECRET_GITHUB" type="string" default="none" optional>
|
||||
@ -109,23 +125,30 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CLIENT_ID_BITBUCKET" type="string" default="none" optional>
|
||||
OAuth2 client ID for BitBucket integration
|
||||
OAuth2 client ID for BitBucket integration
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="CLIENT_SECRET_BITBUCKET" type="string" default="none" optional>
|
||||
OAuth2 client secret for BitBucket integration
|
||||
</ParamField>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Auth Integrations">
|
||||
To integrate with external auth providers, provide value for the related keys
|
||||
<ParamField query="JWT_PROVIDER_AUTH_SECRET" type="string" required>
|
||||
Must be a random 16 byte hex string. Can be generated with `openssl rand -hex 16`
|
||||
</ParamField>
|
||||
<ParamField query="CLIENT_ID_GOOGLE" type="string" default="none" optional>
|
||||
OAuth2 client ID for Google auth integration
|
||||
<ParamField query="CLIENT_ID_GOOGLE_LOGIN" type="string" default="none" optional>
|
||||
OAuth2 client ID for Google login
|
||||
</ParamField>
|
||||
<ParamField query="CLIENT_SECRET_GOOGLE" type="string" default="none" optional>
|
||||
OAuth2 client secret for Google auth integration
|
||||
<ParamField query="CLIENT_SECRET_GOOGLE_LOGIN" type="string" default="none" optional>
|
||||
OAuth2 client secret for Google login
|
||||
</ParamField>
|
||||
<ParamField query="CLIENT_ID_GITHUB_LOGIN" type="string" default="none" optional>
|
||||
OAuth2 client ID for GitHub login
|
||||
</ParamField>
|
||||
<ParamField query="CLIENT_SECRET_GITHUB_LOGIN" type="string" default="none" optional>
|
||||
OAuth2 client secret for GitHub login
|
||||
</ParamField>
|
||||
</Tab>
|
||||
<Tab title="Others">
|
||||
@ -150,18 +173,44 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
JWT token lifetime expressed in seconds or a string describing a time span
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="MONGO_USERNAME" type="string" default="none" optional></ParamField>
|
||||
{" "}
|
||||
|
||||
<ParamField query="MONGO_PASSWORD" type="string" default="none" optional></ParamField>
|
||||
<ParamField
|
||||
query="MONGO_USERNAME"
|
||||
type="string"
|
||||
default="none"
|
||||
optional
|
||||
></ParamField>
|
||||
|
||||
#### Error logging
|
||||
Infisical uses Sentry to report error logs
|
||||
<ParamField query="SENTRY_DSN" type="string" default="none" optional></ParamField>
|
||||
{" "}
|
||||
|
||||
#### Settings
|
||||
<ParamField query="INVITE_ONLY_SIGNUP" type="string" default="false" optional>
|
||||
Only allow users who are invited to sign up
|
||||
</ParamField>
|
||||
<ParamField
|
||||
query="MONGO_PASSWORD"
|
||||
type="string"
|
||||
default="none"
|
||||
optional
|
||||
></ParamField>
|
||||
|
||||
#### Error logging
|
||||
|
||||
Infisical uses Sentry to report error logs
|
||||
|
||||
{" "}
|
||||
|
||||
<ParamField
|
||||
query="SENTRY_DSN"
|
||||
type="string"
|
||||
default="none"
|
||||
optional
|
||||
></ParamField>
|
||||
|
||||
#### Settings
|
||||
|
||||
{" "}
|
||||
|
||||
<ParamField query="INVITE_ONLY_SIGNUP" type="string" default="false" optional>
|
||||
Only allow users who are invited to sign up
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SITE_URL" type="string" default="none" optional>
|
||||
Site URL - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
|
||||
@ -170,6 +219,11 @@ Other environment variables are listed below to increase the functionality of yo
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Frontend environment variables
|
||||
<ParamField query="TELEMETRY_ENABLED" type="string" default="true" optional></ParamField>
|
||||
|
||||
<ParamField
|
||||
query="TELEMETRY_ENABLED"
|
||||
type="string"
|
||||
default="true"
|
||||
optional
|
||||
></ParamField>
|
||||
|
@ -7,9 +7,6 @@ description: "Use our Helm chart to Install Infisical on your Kubernetes cluster
|
||||
- Installed [Helm package manager](https://helm.sh/) version v3.11.3 or greater
|
||||
- You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster
|
||||
|
||||
<iframe width="100%" height="375" src="https://www.youtube.com/embed/ugJZSCcZaV8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
|
||||
By deploying Infisical on Kubernetes, you can take advantage of its features to ensure that the application is fault-tolerant, highly available, and scalable.
|
||||
To make the installation process easier and more streamlined, we have created a Helm chart that you can use to install Infisical on Kubernetes.
|
||||
|
||||
@ -34,10 +31,11 @@ By default, the application will use the latest tag to retrieve the required Doc
|
||||
However, it's important to specify a particular version of Infisical during installation to prevent any significant updates from disrupting your deployment.
|
||||
View [properties for frontend and backend](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical#parameters).
|
||||
|
||||
|
||||
To determine the appropriate versions to use for the docker images, follow the links bellow
|
||||
- [frontend Docker image](https://hub.docker.com/r/infisical/frontend/tags)
|
||||
- [backend Docker image](https://hub.docker.com/r/infisical/backend/tags)
|
||||
<Tip>
|
||||
To find the latest version number of Infisical, follow the links bellow
|
||||
- [frontend Docker image](https://hub.docker.com/r/infisical/frontend/tags)
|
||||
- [backend Docker image](https://hub.docker.com/r/infisical/backend/tags)
|
||||
</Tip>
|
||||
|
||||
```yaml simple-values-example.yaml
|
||||
frontend:
|
||||
@ -45,14 +43,14 @@ frontend:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/frontend
|
||||
tag: "v0.1.3"
|
||||
tag: "v0.26.0" # <--- frontend version
|
||||
pullPolicy: Always
|
||||
|
||||
backend:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/backend
|
||||
tag: "v0.1.3"
|
||||
tag: "v0.26.0" # <--- backend version
|
||||
pullPolicy: Always
|
||||
```
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faGithub,faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "../v2";
|
||||
@ -24,12 +26,26 @@ export default function InitialSignupStep({
|
||||
window.open("/api/v1/sso/redirect/google");
|
||||
window.close();
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||
className="h-14 w-full mx-0"
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
|
||||
className="h-12 w-full mx-0"
|
||||
>
|
||||
{t("signup.continue-with-google")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
window.open("/api/v1/sso/redirect/github");
|
||||
window.close();
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
|
||||
className="h-12 w-full mx-0"
|
||||
>
|
||||
Continue with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
@ -37,10 +53,10 @@ export default function InitialSignupStep({
|
||||
onClick={() => {
|
||||
setIsSignupWithEmail(true);
|
||||
}}
|
||||
isFullWidth
|
||||
className="h-14 w-full mx-0"
|
||||
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
|
||||
className="h-12 w-full mx-0"
|
||||
>
|
||||
Sign Up with email
|
||||
Continue with Email
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||
@ -48,10 +64,10 @@ export default function InitialSignupStep({
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => router.push("/saml-sso")}
|
||||
isFullWidth
|
||||
className="h-14 w-full mx-0"
|
||||
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
|
||||
className="h-12 w-full mx-0"
|
||||
>
|
||||
Continue with SAML SSO
|
||||
Continue with SSO
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] px-8 text-center mt-6 text-xs text-bunker-400'>
|
||||
|
@ -51,10 +51,10 @@ const buttonVariants = cva(
|
||||
false: ""
|
||||
},
|
||||
size: {
|
||||
xs: ["text-xs", "py-1", "px-1"],
|
||||
sm: ["text-sm", "py-2", "px-2"],
|
||||
md: ["text-md", "py-2", "px-4"],
|
||||
lg: ["text-lg", "py-2", "px-8"]
|
||||
xs: ["text-xs", "py-1", "px-2"],
|
||||
sm: ["text-sm", "py-2", "px-4"],
|
||||
md: ["text-md", "py-2", "px-5"],
|
||||
lg: ["text-lg", "py-2", "px-6"]
|
||||
}
|
||||
},
|
||||
compoundVariants: [
|
||||
@ -186,16 +186,17 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
className="absolute rounded-xl opacity-80"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
loadingToggleClass,
|
||||
leftIcon && "ml-2",
|
||||
size === "xs" ? "mr-1" : "mr-2"
|
||||
)}
|
||||
>
|
||||
{leftIcon}
|
||||
</div>
|
||||
{leftIcon && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
loadingToggleClass,
|
||||
size === "xs" ? "mr-1" : "mr-2"
|
||||
)}
|
||||
>
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={twMerge(
|
||||
"transition-all",
|
||||
@ -205,15 +206,16 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
loadingToggleClass,
|
||||
size === "xs" ? "ml-1" : "ml-2"
|
||||
)}
|
||||
>
|
||||
{rightIcon}
|
||||
</div>
|
||||
{rightIcon && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
loadingToggleClass
|
||||
)}
|
||||
>
|
||||
{rightIcon}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { forwardRef, InputHTMLAttributes, ReactNode } from "react";
|
||||
import { ChangeEvent, forwardRef, InputHTMLAttributes, ReactNode } from "react";
|
||||
import { cva, VariantProps } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@ -10,6 +10,7 @@ type Props = {
|
||||
rightIcon?: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
autoCapitalization?: boolean;
|
||||
};
|
||||
|
||||
const inputVariants = cva(
|
||||
@ -80,10 +81,18 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
variant = "filled",
|
||||
size = "md",
|
||||
isReadOnly,
|
||||
autoCapitalization,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
): JSX.Element => {
|
||||
const handleInput = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (autoCapitalization) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.target.value = event.target.value.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
|
||||
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
|
||||
@ -93,6 +102,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
ref={ref}
|
||||
readOnly={isReadOnly}
|
||||
disabled={isDisabled}
|
||||
onInput={handleInput}
|
||||
className={twMerge(
|
||||
leftIcon ? "pl-10" : "pl-2.5",
|
||||
rightIcon ? "pr-10" : "pr-2.5",
|
||||
|
@ -47,9 +47,10 @@ const ActivityLogsRow = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderUser = () => {
|
||||
|
||||
if (row?.user) return `${row.user}`;
|
||||
if (row?.serviceAccount) return `Service Account: ${row.serviceAccount.name}`;
|
||||
if (row?.serviceTokenData.name) return `Service Token: ${row.serviceTokenData.name}`;
|
||||
if (row?.serviceTokenData?.name) return `Service Token: ${row.serviceTokenData.name}`;
|
||||
|
||||
return "";
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ export {
|
||||
useGetOrganization,
|
||||
useGetOrgBillingDetails,
|
||||
useGetOrgInvoices,
|
||||
useGetOrgLicenses,
|
||||
useGetOrgPlanBillingInfo,
|
||||
useGetOrgPlansTable,
|
||||
useGetOrgPlanTable,
|
||||
@ -14,5 +15,4 @@ export {
|
||||
useGetOrgTaxIds,
|
||||
useGetOrgTrialUrl,
|
||||
useRenameOrg,
|
||||
useUpdateOrgBillingDetails
|
||||
} from "./queries";
|
||||
useUpdateOrgBillingDetails} from "./queries";
|
||||
|
@ -5,14 +5,14 @@ import { apiRequest } from "@app/config/request";
|
||||
import {
|
||||
BillingDetails,
|
||||
Invoice,
|
||||
License,
|
||||
Organization,
|
||||
OrgPlanTable,
|
||||
PlanBillingInfo,
|
||||
PmtMethod,
|
||||
ProductsTable,
|
||||
RenameOrgDTO,
|
||||
TaxID
|
||||
} from "./types";
|
||||
TaxID} from "./types";
|
||||
|
||||
const organizationKeys = {
|
||||
getUserOrganization: ["organization"] as const,
|
||||
@ -23,6 +23,7 @@ const organizationKeys = {
|
||||
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const,
|
||||
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
|
||||
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const,
|
||||
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const
|
||||
};
|
||||
|
||||
export const useGetOrganization = () => {
|
||||
@ -311,4 +312,20 @@ export const useCreateCustomerPortalSession = () => {
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const useGetOrgLicenses = (organizationId: string) => {
|
||||
return useQuery({
|
||||
queryKey: organizationKeys.getOrgLicenses(organizationId),
|
||||
queryFn: async () => {
|
||||
if (organizationId === "") return undefined;
|
||||
|
||||
const { data } = await apiRequest.get<License[]>(
|
||||
`/api/v1/organizations/${organizationId}/licenses`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
}
|
@ -49,6 +49,17 @@ export type TaxID = {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type License = {
|
||||
_id: string;
|
||||
customerId: string;
|
||||
prefix: string;
|
||||
licenseKey: string;
|
||||
isActivated: boolean;
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type OrgPlanTableHead = {
|
||||
name: string;
|
||||
}
|
||||
|
@ -54,13 +54,14 @@ export const useGetProjectSecrets = ({
|
||||
env,
|
||||
decryptFileKey,
|
||||
isPaused,
|
||||
folderId
|
||||
folderId,
|
||||
secretPath
|
||||
}: GetProjectSecretsDTO) =>
|
||||
useQuery({
|
||||
// wait for all values to be available
|
||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId || secretPath),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||
select: useCallback(
|
||||
(data: EncryptedSecret[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
|
@ -293,12 +293,10 @@ export const useRevokeMySessions = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
console.log("useRevokeAllSessions 1");
|
||||
const { data } = await apiRequest.delete(
|
||||
"/api/v2/users/me/sessions"
|
||||
);
|
||||
|
||||
console.log("useRevokeAllSessions 2: ", data);
|
||||
return data;
|
||||
},
|
||||
onSuccess() {
|
||||
|
@ -1,3 +1,4 @@
|
||||
export { useDebounce } from "./useDebounce";
|
||||
export { useLeaveConfirm } from "./useLeaveConfirm";
|
||||
export { usePersistentState } from "./usePersistentState";
|
||||
export { usePopUp } from "./usePopUp";
|
||||
|
26
frontend/src/hooks/useDebounce.tsx
Normal file
26
frontend/src/hooks/useDebounce.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// Ref: https://usehooks.com/useDebounce/
|
||||
export const useDebounce = <T extends unknown>(value: T, delay = 500): T => {
|
||||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
},
|
||||
[value, delay] // Only re-call effect if value or delay changes
|
||||
);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
@ -740,7 +740,7 @@ export const DashboardPage = () => {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<form autoComplete="off" className="h-full">
|
||||
<form autoComplete="off" className="h-full flex flex-col">
|
||||
{/* breadcrumb row */}
|
||||
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||
<NavHeader
|
||||
@ -924,8 +924,8 @@ export const DashboardPage = () => {
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
isEmptyPage ? "flex flex-col items-center justify-center" : ""
|
||||
} no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
||||
isEmptyPage ? "flex flex-col flex-grow items-center justify-center" : ""
|
||||
} no-scrollbar::-webkit-scrollbar mt-3 flex flex-col overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
||||
ref={secretContainer}
|
||||
>
|
||||
{!isEmptyPage && (
|
||||
@ -935,7 +935,7 @@ export const DashboardPage = () => {
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
>
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar flex-grow">
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-96 overflow-y-auto">
|
||||
@ -971,6 +971,7 @@ export const DashboardPage = () => {
|
||||
register={register}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
/>
|
||||
))}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
@ -1016,9 +1017,12 @@ export const DashboardPage = () => {
|
||||
</FormProvider>
|
||||
|
||||
<SecretDropzone
|
||||
workspaceId={workspaceId}
|
||||
isSmaller={!isEmptyPage}
|
||||
onParsedEnv={handleUploadedEnv}
|
||||
onAddNewSecret={onAppendSecret}
|
||||
environments={userAvailableEnvs}
|
||||
decryptFileKey={latestFileKey!}
|
||||
/>
|
||||
</div>
|
||||
{/* secrets table and drawers, modals */}
|
||||
|
@ -75,6 +75,13 @@ export type FormData = yup.InferType<typeof schema>;
|
||||
export type TSecretDetailsOpen = { index: number; id: string };
|
||||
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
|
||||
|
||||
// to convert multi line into single line ones by quoting them and changing to string \n
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
};
|
||||
|
||||
export const downloadSecret = (
|
||||
secrets: FormData["secrets"] = [],
|
||||
importedSecrets: { key: string; value?: string; comment?: string }[] = [],
|
||||
@ -86,9 +93,11 @@ export const downloadSecret = (
|
||||
});
|
||||
const finalSecret = [...importedSecrets];
|
||||
secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => {
|
||||
const finalVal =
|
||||
overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value;
|
||||
const newValue = {
|
||||
key,
|
||||
value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
|
||||
value: formatMultiValueEnv(finalVal),
|
||||
comment
|
||||
};
|
||||
// can also be zero thus failing
|
||||
|
@ -1,26 +1,128 @@
|
||||
import { ChangeEvent, DragEvent } from "react";
|
||||
import { ChangeEvent, DragEvent, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faClone,
|
||||
faKey,
|
||||
faSearch,
|
||||
faSquareXmark,
|
||||
faUpload
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionalityj
|
||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
||||
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
Select,
|
||||
SelectItem,
|
||||
Skeleton,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
||||
import { UserWsKeyPair } from "@app/hooks/api/types";
|
||||
|
||||
const formSchema = yup.object({
|
||||
environment: yup.string().required().label("Environment").trim(),
|
||||
secretPath: yup
|
||||
.string()
|
||||
.required()
|
||||
.label("Secret Path")
|
||||
.trim()
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
),
|
||||
secrets: yup.lazy((val) => {
|
||||
const valSchema: Record<string, yup.StringSchema> = {};
|
||||
Object.keys(val).forEach((key) => {
|
||||
valSchema[key] = yup.string().trim();
|
||||
});
|
||||
return yup.object(valSchema);
|
||||
})
|
||||
});
|
||||
|
||||
type TFormSchema = yup.InferType<typeof formSchema>;
|
||||
|
||||
const parseJson = (src: ArrayBuffer) => {
|
||||
const file = src.toString();
|
||||
const formatedData: Record<string, string> = JSON.parse(file);
|
||||
const env: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(formatedData).forEach((key) => {
|
||||
if (typeof formatedData[key] === "string") {
|
||||
env[key] = { value: formatedData[key], comments: [] };
|
||||
}
|
||||
});
|
||||
return env;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isSmaller: boolean;
|
||||
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
|
||||
onAddNewSecret?: () => void;
|
||||
environments?: { name: string; slug: string }[];
|
||||
workspaceId: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props): JSX.Element => {
|
||||
export const SecretDropzone = ({
|
||||
isSmaller,
|
||||
onParsedEnv,
|
||||
onAddNewSecret,
|
||||
environments = [],
|
||||
workspaceId,
|
||||
decryptFileKey
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [isDragActive, setDragActive] = useToggle();
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["importSecEnv"] as const);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
register,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isDirty }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: yupResolver(formSchema),
|
||||
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
||||
});
|
||||
|
||||
const secretPath = watch("secretPath");
|
||||
const selectedEnvSlug = watch("environment");
|
||||
const debouncedSecretPath = useDebounce(secretPath);
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
env: selectedEnvSlug,
|
||||
secretPath: debouncedSecretPath,
|
||||
isPaused: !(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)),
|
||||
decryptFileKey
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setValue("secrets", {});
|
||||
setSearchFilter("");
|
||||
}, [debouncedSecretPath]);
|
||||
|
||||
const handleDrag = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
@ -32,7 +134,7 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
}
|
||||
};
|
||||
|
||||
const parseFile = (file?: File) => {
|
||||
const parseFile = (file?: File, isJson?: boolean) => {
|
||||
const reader = new FileReader();
|
||||
if (!file) {
|
||||
createNotification({
|
||||
@ -47,7 +149,9 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
reader.onload = (event) => {
|
||||
if (!event?.target?.result) return;
|
||||
// parse function's argument looks like to be ArrayBuffer
|
||||
const env = parseDotEnv(event.target.result as ArrayBuffer);
|
||||
const env = isJson
|
||||
? parseJson(event.target.result as ArrayBuffer)
|
||||
: parseDotEnv(event.target.result as ArrayBuffer);
|
||||
setIsLoading.off();
|
||||
onParsedEnv(env);
|
||||
};
|
||||
@ -74,7 +178,31 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
|
||||
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
parseFile(e.target?.files?.[0]);
|
||||
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
|
||||
};
|
||||
|
||||
const handleFormSubmit = (data: TFormSchema) => {
|
||||
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
||||
Object.keys(data.secrets || {}).forEach((key) => {
|
||||
if (data.secrets[key]) {
|
||||
secretsToBePulled[key] = {
|
||||
value: (shouldIncludeValues && data.secrets[key]) || "",
|
||||
comments: [""]
|
||||
};
|
||||
}
|
||||
});
|
||||
onParsedEnv(secretsToBePulled);
|
||||
handlePopUpClose("importSecEnv");
|
||||
reset();
|
||||
};
|
||||
|
||||
const handleSecSelectAll = () => {
|
||||
if (secrets?.secrets) {
|
||||
setValue(
|
||||
"secrets",
|
||||
secrets?.secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -84,9 +212,9 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
className={twMerge(
|
||||
"relative mx-0.5 mb-4 mt-4 flex w-full max-w-[calc(100vw-292px)] cursor-pointer items-center justify-center space-x-2 rounded-md bg-mineshaft-900 py-8 px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 text-sm px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||
isDragActive && "opacity-100",
|
||||
!isSmaller && "max-w-3xl flex-col space-y-4 py-20",
|
||||
!isSmaller && "w-full max-w-3xl flex-col space-y-4 py-20",
|
||||
isLoading && "bg-bunker-800"
|
||||
)}
|
||||
>
|
||||
@ -95,35 +223,184 @@ export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props
|
||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
||||
</div>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
accept=".txt,.env,.yml,.yaml"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
{!isSmaller && (
|
||||
<>
|
||||
<div className="flex w-full flex-row items-center justify-center py-4">
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex items-center justify-cente flex-col space-y-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
||||
</div>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
accept=".txt,.env,.yml,.yaml,.json"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full flex-row items-center justify-center py-4",
|
||||
isSmaller && "py-1"
|
||||
)}
|
||||
>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-8">
|
||||
<Modal
|
||||
isOpen={popUp.importSecEnv.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("importSecEnv", isOpen);
|
||||
reset();
|
||||
setSearchFilter("");
|
||||
}}
|
||||
>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="star" size={isSmaller ? "xs" : "sm"}>
|
||||
Copy Secrets From An Environment
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
title="Copy Secret From An Environment"
|
||||
subTitle="Copy/paste secrets from other environments into this context"
|
||||
>
|
||||
<form>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<FormControl label="Environment" isRequired className="w-1/3">
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
position="popper"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormControl label="Secret Path" className="flex-grow" isRequired>
|
||||
<Input
|
||||
{...register("secretPath")}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="border-t border-mineshaft-600 pt-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>Secrets</div>
|
||||
<div className="w-1/2 flex items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Search for secret"
|
||||
value={searchFilter}
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||
onChange={(evt) => setSearchFilter(evt.target.value)}
|
||||
/>
|
||||
<Tooltip content="Select All">
|
||||
<IconButton
|
||||
ariaLabel="Select all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={handleSecSelectAll}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Unselect All">
|
||||
<IconButton
|
||||
ariaLabel="UnSelect all"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{!isSecretsLoading && !secrets?.secrets?.length && (
|
||||
<EmptyState title="No secrets found" icon={faKey} />
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4 max-h-64 overflow-auto thin-scrollbar ">
|
||||
{isSecretsLoading &&
|
||||
Array.apply(0, Array(2)).map((_x, i) => (
|
||||
<Skeleton
|
||||
key={`secret-pull-loading-${i + 1}`}
|
||||
className="bg-mineshaft-700"
|
||||
/>
|
||||
))}
|
||||
|
||||
{secrets?.secrets
|
||||
?.filter(({ key }) =>
|
||||
key.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
)
|
||||
?.map(({ _id, key, value: secVal }) => (
|
||||
<Controller
|
||||
key={`pull-secret--${_id}`}
|
||||
control={control}
|
||||
name={`secrets.${key}`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
id={`pull-secret-${_id}`}
|
||||
isChecked={Boolean(value)}
|
||||
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 mb-4">
|
||||
<Checkbox
|
||||
id="populate-include-value"
|
||||
isChecked={shouldIncludeValues}
|
||||
onCheckedChange={(isChecked) =>
|
||||
setShouldIncludeValues(isChecked as boolean)
|
||||
}
|
||||
>
|
||||
Include secret values
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faClone} />}
|
||||
type="submit"
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
Paste Secrets
|
||||
</Button>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isSmaller && (
|
||||
<Button variant="star" onClick={onAddNewSecret}>
|
||||
Add a new secret
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}{" "}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -79,6 +79,7 @@ type Props = {
|
||||
setValue: UseFormSetValue<FormData>;
|
||||
isKeyError?: boolean;
|
||||
keyError?: string;
|
||||
autoCapitalization?: boolean;
|
||||
};
|
||||
|
||||
export const SecretInputRow = memo(
|
||||
@ -98,7 +99,8 @@ export const SecretInputRow = memo(
|
||||
setValue,
|
||||
isKeyError,
|
||||
keyError,
|
||||
secUniqId
|
||||
secUniqId,
|
||||
autoCapitalization
|
||||
}: Props): JSX.Element => {
|
||||
const isKeySubDisabled = useRef<boolean>(false);
|
||||
// comment management in a row
|
||||
@ -243,6 +245,7 @@ export const SecretInputRow = memo(
|
||||
isKeySubDisabled.current = false;
|
||||
field.onBlur();
|
||||
}}
|
||||
autoCapitalization={autoCapitalization}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
@ -278,7 +281,6 @@ export const SecretInputRow = memo(
|
||||
value={editorRef}
|
||||
isVisible={!isSecretValueHidden}
|
||||
onChange={(val, html) => {
|
||||
console.log(val);
|
||||
onChange(val);
|
||||
setEditorRef(html);
|
||||
}}
|
||||
|
@ -2,7 +2,8 @@ import { FormEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faGithub,faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import axios from "axios"
|
||||
|
||||
@ -34,7 +35,6 @@ export const InitialStep = ({
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const [loginEmailChosen, setLoginEmailChosen] = useState(false);
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@ -68,8 +68,7 @@ export const InitialStep = ({
|
||||
|
||||
// send request to server endpoint
|
||||
const instance = axios.create()
|
||||
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
|
||||
console.log(cliResp)
|
||||
await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
|
||||
|
||||
// cli page
|
||||
router.push("/cli-redirect");
|
||||
@ -121,22 +120,53 @@ export const InitialStep = ({
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant={loginEmailChosen ? "outline_bg" : "solid"}
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
|
||||
window.open(`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
|
||||
window.close();
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||
className="h-12 w-full mx-0"
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
|
||||
className="h-11 w-full mx-0"
|
||||
>
|
||||
{t("login.continue-with-google")}
|
||||
</Button>
|
||||
</div>
|
||||
{loginEmailChosen && <>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
|
||||
window.open(`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
|
||||
|
||||
window.close();
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
|
||||
className="h-11 w-full mx-0"
|
||||
>
|
||||
Continue with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
setStep(2);
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
|
||||
className="h-11 w-full mx-0"
|
||||
>
|
||||
Continue with SSO
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'>
|
||||
<div className='w-full border-t border-mineshaft-500' />
|
||||
<div className='w-full border-t border-mineshaft-400/60' />
|
||||
<span className="mx-2 text-mineshaft-200 text-xs">or</span>
|
||||
<div className='w-full border-t border-mineshaft-400/60' />
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md'>
|
||||
<Input
|
||||
@ -146,7 +176,7 @@ export const InitialStep = ({
|
||||
placeholder="Enter your email..."
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
className="h-12"
|
||||
className="h-11"
|
||||
/>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
@ -158,49 +188,21 @@ export const InitialStep = ({
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
className="h-12 select:-webkit-autofill:focus"
|
||||
className="h-11 select:-webkit-autofill:focus"
|
||||
/>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-5'>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-12'
|
||||
className='h-11'
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
isLoading={isLoading}
|
||||
> Continue with Email </Button>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
|
||||
<div className='w-full border-t border-mineshaft-500' />
|
||||
</div></>}
|
||||
{!loginEmailChosen && <div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLoginEmailChosen(true);
|
||||
}}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-12'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
> Continue with Email </Button>
|
||||
</div>}
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
setStep(2);
|
||||
}}
|
||||
isFullWidth
|
||||
className="h-12 w-full mx-0"
|
||||
>
|
||||
Continue with SAML SSO
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
!serverDetails?.inviteOnlySignup ?
|
||||
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
|
||||
|
@ -33,7 +33,7 @@ export const CurrentPlanSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current Usage</h2>
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current usage</h2>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { LicensesSection } from "./LicensesSection";
|
||||
|
||||
export const BillingSelfHostedTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<LicensesSection />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import { faFileContract } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useGetOrgLicenses
|
||||
} from "@app/hooks/api";
|
||||
|
||||
export const LicensesSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data, isLoading } = useGetOrgLicenses(currentOrg?._id ?? "");
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Enterprise licenses</h2>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>License Key</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Issued Date</Th>
|
||||
<Th>Expiry Date</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading && data && data?.length > 0 && data.map(({
|
||||
_id,
|
||||
licenseKey,
|
||||
isActivated,
|
||||
createdAt,
|
||||
expiresAt
|
||||
}) => {
|
||||
const formattedCreatedAt = new Date(createdAt).toISOString().split("T")[0];
|
||||
const formattedExpiresAt = new Date(expiresAt).toISOString().split("T")[0];
|
||||
return (
|
||||
<Tr key={`license-${_id}`} className="h-10">
|
||||
<Td>{licenseKey}</Td>
|
||||
<Td>{isActivated ? "Active" : "Inactive"}</Td>
|
||||
<Td>{formattedCreatedAt}</Td>
|
||||
<Td>{formattedExpiresAt}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="licenses" />}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={4}>
|
||||
<EmptyState title="No enterprise licenses on file" icon={faFileContract} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from "./BillingSelfHostedTab";
|
@ -4,9 +4,11 @@ import { Tab } from "@headlessui/react"
|
||||
import { BillingCloudTab } from "../BillingCloudTab";
|
||||
import { BillingDetailsTab } from "../BillingDetailsTab";
|
||||
import { BillingReceiptsTab } from "../BillingReceiptsTab";
|
||||
import { BillingSelfHostedTab } from "../BillingSelfHostedTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Infisical Cloud", key: "tab-infisical-cloud" },
|
||||
{ name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" },
|
||||
{ name: "Receipts", key: "tab-receipts" },
|
||||
{ name: "Billing details", key: "tab-billing-details" }
|
||||
];
|
||||
@ -32,6 +34,9 @@ export const BillingTabGroup = () => {
|
||||
<Tab.Panel>
|
||||
<BillingCloudTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingSelfHostedTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingReceiptsTab />
|
||||
</Tab.Panel>
|
||||
|
@ -17,7 +17,10 @@ import {
|
||||
const authMethods = [
|
||||
{ label: "Email", value: "email" },
|
||||
{ label: "Google SSO", value: "google" },
|
||||
{ label: "Okta SAML 2.0", value: "okta-saml" },
|
||||
{ label: "GitHub SSO", value: "github" },
|
||||
{ label: "Okta SAML", value: "okta-saml" },
|
||||
{ label: "Azure SAML", value: "azure-saml" },
|
||||
{ label: "JumpCloud SAML", value: "jumpcloud-saml" }
|
||||
];
|
||||
|
||||
const schema = yup.object({
|
||||
@ -54,9 +57,13 @@ export const AuthMethodSection = () => {
|
||||
authMethod
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (authMethod === "okta-saml") {
|
||||
if (
|
||||
authMethod === "okta-saml"
|
||||
|| authMethod === "azure-saml"
|
||||
|| authMethod === "jumpcloud-saml"
|
||||
) {
|
||||
createNotification({
|
||||
text: "Okta SAML 2.0 can only be configured in your organization settings",
|
||||
text: "SAML authentication can only be configured in your organization settings",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
|
@ -3,7 +3,7 @@ import crypto from "crypto";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faInfoCircle, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import jsrp from "jsrp";
|
||||
import nacl from "tweetnacl";
|
||||
@ -198,7 +198,7 @@ export const UserInfoSSOStep = ({
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
localStorage.setItem("projectData.id", project._id);
|
||||
|
||||
setStep(1);
|
||||
setStep(1);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
@ -256,7 +256,7 @@ export const UserInfoSSOStep = ({
|
||||
)}
|
||||
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
|
||||
<InputField
|
||||
label={t("section.password.password")}
|
||||
label="Infisical Password"
|
||||
onChangeHandler={(pass: string) => {
|
||||
setPassword(pass);
|
||||
checkPassword({
|
||||
@ -272,6 +272,7 @@ export const UserInfoSSOStep = ({
|
||||
autoComplete="new-password"
|
||||
id="new-password"
|
||||
/>
|
||||
<div className="mt-2 w-min min-w-[20rem] max-h-60 flex-col items-center justify-center rounded-md px-1.5 bg-mineshaft-500 text-mineshaft-300 text-xs p-1.5"><FontAwesomeIcon icon={faInfoCircle} className="mr-1.5" />Infisical Password is used as part of the encryption mechanism so that even the authentication provider is not able to access your secrets.</div>
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
||||
<div className="mb-2 text-sm text-gray-400">{t("section.password.validate-base")}</div>
|
||||
@ -307,7 +308,7 @@ export const UserInfoSSOStep = ({
|
||||
onClick={signupErrorCheck}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-14'
|
||||
className='h-12'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
|
Reference in New Issue
Block a user