mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-20 01:48:03 +00:00
Compare commits
16 Commits
audit-log-
...
add-access
Author | SHA1 | Date | |
---|---|---|---|
5e85de3937 | |||
8719e3e75e | |||
69ece1f3e3 | |||
d5cd6f79f9 | |||
19c0731166 | |||
f636cc678b | |||
ff8ad14e1b | |||
d683d3adb3 | |||
d9b8cd1204 | |||
27b5e2aa68 | |||
692121445d | |||
9a940dce64 | |||
7e523546b3 | |||
814d6e2709 | |||
ba57899a56 | |||
aef3a7436f |
@ -4,6 +4,7 @@ import "ts-node/register";
|
||||
import dotenv from "dotenv";
|
||||
import type { Knex } from "knex";
|
||||
import path from "path";
|
||||
import { initLogger } from "@app/lib/logger";
|
||||
|
||||
// Update with your config settings. .
|
||||
dotenv.config({
|
||||
@ -13,6 +14,8 @@ dotenv.config({
|
||||
path: path.join(__dirname, "../../../.env")
|
||||
});
|
||||
|
||||
initLogger();
|
||||
|
||||
export default {
|
||||
development: {
|
||||
client: "postgres",
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const MIGRATION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const result = await knex.raw("SHOW statement_timeout");
|
||||
const originalTimeout = result.rows[0].statement_timeout;
|
||||
|
||||
try {
|
||||
await knex.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`);
|
||||
|
||||
// iat means IdentityAccessToken
|
||||
await knex.raw(`
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_iat_identity_id
|
||||
ON ${TableName.IdentityAccessToken} ("identityId")
|
||||
`);
|
||||
|
||||
await knex.raw(`
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_iat_ua_client_secret_id
|
||||
ON ${TableName.IdentityAccessToken} ("identityUAClientSecretId")
|
||||
`);
|
||||
} finally {
|
||||
await knex.raw(`SET statement_timeout = '${originalTimeout}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const result = await knex.raw("SHOW statement_timeout");
|
||||
const originalTimeout = result.rows[0].statement_timeout;
|
||||
|
||||
try {
|
||||
await knex.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`);
|
||||
|
||||
await knex.raw(`
|
||||
DROP INDEX IF EXISTS idx_iat_identity_id
|
||||
`);
|
||||
|
||||
await knex.raw(`
|
||||
DROP INDEX IF EXISTS idx_iat_ua_client_secret_id
|
||||
`);
|
||||
} finally {
|
||||
await knex.raw(`SET statement_timeout = '${originalTimeout}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export const config = { transaction: false };
|
@ -108,16 +108,16 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const threeMonthsAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
||||
const twelveMonthsAgo = new Date(now.getTime() - 360 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const memberships = await db
|
||||
.replicaNode()(TableName.OrgMembership)
|
||||
.where("status", "invited")
|
||||
.where((qb) => {
|
||||
// lastInvitedAt is null AND createdAt is between 1 week and 3 months ago
|
||||
// lastInvitedAt is null AND createdAt is between 1 week and 12 months ago
|
||||
void qb
|
||||
.whereNull(`${TableName.OrgMembership}.lastInvitedAt`)
|
||||
.whereBetween(`${TableName.OrgMembership}.createdAt`, [threeMonthsAgo, oneWeekAgo]);
|
||||
.whereBetween(`${TableName.OrgMembership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
// lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago
|
||||
|
@ -36,6 +36,8 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||
import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import {
|
||||
BadRequestError,
|
||||
ForbiddenRequestError,
|
||||
@ -44,9 +46,10 @@ import {
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
@ -1438,6 +1441,8 @@ export const orgServiceFactory = ({
|
||||
* Re-send emails to users who haven't accepted an invite yet
|
||||
*/
|
||||
const notifyInvitedUsers = async () => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: notify invited users started`);
|
||||
|
||||
const invitedUsers = await orgMembershipDAL.findRecentInvitedMemberships();
|
||||
const appCfg = getConfig();
|
||||
|
||||
@ -1461,24 +1466,32 @@ export const orgServiceFactory = ({
|
||||
});
|
||||
|
||||
if (invitedUser.inviteEmail) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: `Reminder: You have been invited to ${org.name} on Infisical`,
|
||||
recipients: [invitedUser.inviteEmail],
|
||||
substitutions: {
|
||||
organizationName: org.name,
|
||||
email: invitedUser.inviteEmail,
|
||||
organizationId: org.id.toString(),
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
notifiedUsers.push(invitedUser.id);
|
||||
await delayMs(Math.max(0, applyJitter(0, 2000)));
|
||||
|
||||
try {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: `Reminder: You have been invited to ${org.name} on Infisical`,
|
||||
recipients: [invitedUser.inviteEmail],
|
||||
substitutions: {
|
||||
organizationName: org.name,
|
||||
email: invitedUser.inviteEmail,
|
||||
organizationId: org.id.toString(),
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
notifiedUsers.push(invitedUser.id);
|
||||
} catch (err) {
|
||||
logger.error(err, `${QueueName.DailyResourceCleanUp}: notify invited users failed to send email`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await orgMembershipDAL.updateLastInvitedAtByIds(notifiedUsers);
|
||||
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: notify invited users completed`);
|
||||
};
|
||||
|
||||
return {
|
||||
|
6
docs/Dockerfile
Normal file
6
docs/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN npm install -g mint
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["mint", "dev"]
|
@ -54,33 +54,55 @@ const getPlan = (subscription: SubscriptionPlan) => {
|
||||
return "Free";
|
||||
};
|
||||
|
||||
const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => {
|
||||
const email = "support@infisical.com";
|
||||
|
||||
const body = `Hello Infisical Support Team,
|
||||
|
||||
Issue Details:
|
||||
[What you did]
|
||||
[What you expected to happen]
|
||||
[What actually happened]
|
||||
[Any error request IDs]
|
||||
[Any supporting screenshots or video recording of the issue/request at hand]
|
||||
|
||||
Account Info:
|
||||
- Organization ID: ${variables.org_id}
|
||||
- Domain: ${variables.domain}
|
||||
|
||||
Thank you,
|
||||
[Your Name]`;
|
||||
|
||||
return `mailto:${email}?body=${encodeURIComponent(body)}`;
|
||||
};
|
||||
|
||||
export const INFISICAL_SUPPORT_OPTIONS = [
|
||||
[
|
||||
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||
"Support Forum",
|
||||
"https://infisical.com/slack"
|
||||
() => "https://infisical.com/slack"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
|
||||
"Read Docs",
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
() => "https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
|
||||
"GitHub Issues",
|
||||
"https://github.com/Infisical/infisical/issues"
|
||||
() => "https://github.com/Infisical/infisical/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
||||
"Email Support",
|
||||
"mailto:support@infisical.com"
|
||||
getFormattedSupportEmailLink
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
|
||||
"Instance Admins",
|
||||
"server-admins"
|
||||
() => "server-admins"
|
||||
]
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const Navbar = () => {
|
||||
const { user } = useUser();
|
||||
@ -258,7 +280,15 @@ export const Navbar = () => {
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="bottom" className="mt-3 p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, getUrl]) => {
|
||||
const url =
|
||||
text === "Email Support"
|
||||
? getUrl({
|
||||
org_id: currentOrg.id,
|
||||
domain: window.location.origin
|
||||
})
|
||||
: getUrl();
|
||||
|
||||
if (url === "server-admins" && isInfisicalCloud()) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,19 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faArrowLeft, faInfo, faMobile, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowLeft, faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { WishForm } from "@app/components/features/WishForm";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/NavBar/Navbar";
|
||||
|
||||
export const PersonalSettingsLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -36,37 +28,6 @@ export const PersonalSettingsLayout = () => {
|
||||
<div className="relative mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{envConfig.PLATFORM_VERSION && (
|
||||
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||
Version: {envConfig.PLATFORM_VERSION}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
</nav>
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, Tooltip } from "@app/components/v2";
|
||||
@ -10,41 +8,25 @@ type Props = {
|
||||
label: string;
|
||||
onClear: () => void;
|
||||
children: React.ReactNode;
|
||||
tooltipText?: string;
|
||||
};
|
||||
|
||||
export const LogFilterItem = ({
|
||||
label,
|
||||
onClear,
|
||||
hoverTooltip,
|
||||
children,
|
||||
className,
|
||||
tooltipText
|
||||
}: Props) => {
|
||||
export const LogFilterItem = ({ label, onClear, hoverTooltip, children, className }: Props) => {
|
||||
return (
|
||||
<div className={twMerge("flex flex-col justify-between", className)}>
|
||||
<div className="flex items-center pr-1">
|
||||
<p className="text-xs opacity-60">{label}</p>
|
||||
{tooltipText && (
|
||||
<Tooltip content={tooltipText} className="max-w-sm">
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
className="-mt-[0.05rem] ml-1 text-[11px] text-mineshaft-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => onClear()}
|
||||
variant="link"
|
||||
className="ml-auto font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
|
||||
size="xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
|
||||
<div className={twMerge("flex flex-col justify-between", className)}>
|
||||
<div className="flex items-center justify-between pr-1">
|
||||
<p className="text-xs opacity-60">{label}</p>
|
||||
<Button
|
||||
onClick={() => onClear()}
|
||||
variant="link"
|
||||
className="font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
|
||||
size="xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
|
||||
<div>{children}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
@ -366,7 +366,6 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
|
||||
</LogFilterItem>
|
||||
<LogFilterItem
|
||||
label="Secret Path"
|
||||
tooltipText="Enter the exact secret path (wildcards like * are not supported)"
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by secret path."
|
||||
@ -381,7 +380,10 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl className="w-full">
|
||||
<FormControl
|
||||
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
|
||||
className="w-full"
|
||||
>
|
||||
<Input
|
||||
placeholder="Enter secret path"
|
||||
className="disabled:cursor-not-allowed"
|
||||
@ -401,7 +403,6 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
|
||||
? "Select a project before filtering by secret key."
|
||||
: undefined
|
||||
}
|
||||
tooltipText="Enter the exact secret key name (wildcards like * are not supported)"
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
label="Secret Key"
|
||||
onClear={() => {
|
||||
@ -412,7 +413,10 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
|
||||
control={control}
|
||||
name="secretKey"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl className="w-full">
|
||||
<FormControl
|
||||
tooltipText="Filter audit logs related to a specific secret."
|
||||
className="w-full"
|
||||
>
|
||||
<Input
|
||||
isDisabled={!selectedProject}
|
||||
{...field}
|
||||
|
@ -70,7 +70,7 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
upgrade your subscription
|
||||
</a>
|
||||
</a>
|
||||
)}
|
||||
)}{" "}
|
||||
to view integration logs
|
||||
</p>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user