mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: Re-invite users every 1 week for up to a month.
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
|
||||||
|
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
|
||||||
|
if (!hasColumn) {
|
||||||
|
t.datetime("lastInvitedAt").nullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasColumn = await knex.schema.hasColumn(TableName.OrgMembership, "lastInvitedAt");
|
||||||
|
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
|
||||||
|
if (hasColumn) {
|
||||||
|
t.dropColumn("lastInvitedAt");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@ export const OrgMembershipsSchema = z.object({
|
|||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
roleId: z.string().uuid().nullable().optional(),
|
roleId: z.string().uuid().nullable().optional(),
|
||||||
projectFavorites: z.string().array().nullable().optional(),
|
projectFavorites: z.string().array().nullable().optional(),
|
||||||
isActive: z.boolean().default(true)
|
isActive: z.boolean().default(true),
|
||||||
|
lastInvitedAt: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
||||||
|
|||||||
@@ -1615,7 +1615,8 @@ export const registerRoutes = async (
|
|||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
||||||
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL,
|
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL,
|
||||||
serviceTokenService
|
serviceTokenService,
|
||||||
|
orgService
|
||||||
});
|
});
|
||||||
|
|
||||||
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
||||||
|
|||||||
@@ -103,8 +103,37 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findRecentInvitedMemberships = async () => {
|
||||||
|
try {
|
||||||
|
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 memberships = await db
|
||||||
|
.replicaNode()(TableName.OrgMembership)
|
||||||
|
.where("status", "invited")
|
||||||
|
.where((qb) => {
|
||||||
|
// lastInvitedAt is null AND createdAt is between 1 week and 3 months ago
|
||||||
|
void qb.whereNull("lastInvitedAt").whereBetween("createdAt", [threeMonthsAgo, oneWeekAgo]);
|
||||||
|
})
|
||||||
|
.orWhere((qb) => {
|
||||||
|
// lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago
|
||||||
|
void qb.where("lastInvitedAt", "<", oneWeekAgo).where("createdAt", ">", oneMonthAgo);
|
||||||
|
});
|
||||||
|
|
||||||
|
return memberships;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({
|
||||||
|
error,
|
||||||
|
name: "Find recent invited memberships"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...orgMembershipOrm,
|
...orgMembershipOrm,
|
||||||
findOrgMembershipById
|
findOrgMembershipById,
|
||||||
|
findRecentInvitedMemberships
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ type TOrgServiceFactoryDep = {
|
|||||||
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
||||||
>;
|
>;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey" | "create">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
|
orgMembershipDAL: Pick<
|
||||||
|
TOrgMembershipDALFactory,
|
||||||
|
"findOrgMembershipById" | "findOne" | "findById" | "findRecentInvitedMemberships" | "updateById"
|
||||||
|
>;
|
||||||
incidentContactDAL: TIncidentContactsDALFactory;
|
incidentContactDAL: TIncidentContactsDALFactory;
|
||||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
|
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
|
||||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
|
||||||
@@ -1424,6 +1427,46 @@ export const orgServiceFactory = ({
|
|||||||
return incidentContact;
|
return incidentContact;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-send emails to users who haven't accepted an invite yet
|
||||||
|
*/
|
||||||
|
const notifyInvitedUsers = async () => {
|
||||||
|
const invitedUsers = await orgMembershipDAL.findRecentInvitedMemberships();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
invitedUsers.map(async (invitedUser) => {
|
||||||
|
const org = await orgDAL.findById(invitedUser.orgId);
|
||||||
|
if (!org) return;
|
||||||
|
|
||||||
|
const token = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||||
|
userId: invitedUser.id,
|
||||||
|
orgId: org.id
|
||||||
|
});
|
||||||
|
|
||||||
|
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`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await orgMembershipDAL.updateById(invitedUser.id, {
|
||||||
|
lastInvitedAt: new Date()
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
findOrganizationById,
|
findOrganizationById,
|
||||||
findAllOrgMembers,
|
findAllOrgMembers,
|
||||||
@@ -1447,6 +1490,7 @@ export const orgServiceFactory = ({
|
|||||||
listProjectMembershipsByOrgMembershipId,
|
listProjectMembershipsByOrgMembershipId,
|
||||||
findOrgBySlug,
|
findOrgBySlug,
|
||||||
resendOrgMemberInvitation,
|
resendOrgMemberInvitation,
|
||||||
upgradePrivilegeSystem
|
upgradePrivilegeSystem,
|
||||||
|
notifyInvitedUsers
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
|||||||
|
|
||||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||||
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
||||||
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||||
@@ -24,6 +25,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
|||||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
|
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
|
||||||
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiringTokens">;
|
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiringTokens">;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
|
orgService: TOrgServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyResourceCleanUpQueueServiceFactory>;
|
||||||
@@ -39,7 +41,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL,
|
secretVersionV2DAL,
|
||||||
identityUniversalAuthClientSecretDAL,
|
identityUniversalAuthClientSecretDAL,
|
||||||
serviceTokenService
|
serviceTokenService,
|
||||||
|
orgService
|
||||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||||
@@ -54,6 +57,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
await secretVersionV2DAL.pruneExcessVersions();
|
await secretVersionV2DAL.pruneExcessVersions();
|
||||||
await secretFolderVersionDAL.pruneExcessVersions();
|
await secretFolderVersionDAL.pruneExcessVersions();
|
||||||
await serviceTokenService.notifyExpiringTokens();
|
await serviceTokenService.notifyExpiringTokens();
|
||||||
|
await orgService.notifyInvitedUsers();
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,10 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
const handleCopyToken = async () => {
|
const handleCopyToken = async () => {
|
||||||
try {
|
try {
|
||||||
await window.navigator.clipboard.writeText(getAuthToken());
|
await window.navigator.clipboard.writeText(getAuthToken());
|
||||||
createNotification({ type: "success", text: "Copied current login session token to clipboard" });
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Copied current login session token to clipboard"
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
createNotification({ type: "error", text: "Failed to copy user token to clipboard" });
|
createNotification({ type: "error", text: "Failed to copy user token to clipboard" });
|
||||||
|
|||||||
Reference in New Issue
Block a user