mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 20:27:43 +00:00
Merge pull request #3507 from Infisical/feat/orgUserAuthTokenExpiration
feat(user-auth): make users auth token expiration customizable for orgs
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const tokenDuration = appCfg?.JWT_REFRESH_LIFETIME;
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.string("userTokenExpiration");
|
||||||
|
});
|
||||||
|
if (tokenDuration) {
|
||||||
|
await knex(TableName.Organization).update({ userTokenExpiration: tokenDuration });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration")) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.dropColumn("userTokenExpiration");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||||
bypassOrgAuthEnabled: z.boolean().default(false)
|
bypassOrgAuthEnabled: z.boolean().default(false),
|
||||||
|
userTokenExpiration: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export * from "./array";
|
|||||||
export * from "./dates";
|
export * from "./dates";
|
||||||
export * from "./object";
|
export * from "./object";
|
||||||
export * from "./string";
|
export * from "./string";
|
||||||
|
export * from "./time";
|
||||||
export * from "./undefined";
|
export * from "./undefined";
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import ms, { StringValue } from "ms";
|
||||||
|
|
||||||
|
const convertToMilliseconds = (exp: string | number): number => {
|
||||||
|
if (typeof exp === "number") {
|
||||||
|
return exp * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = ms(exp as StringValue);
|
||||||
|
if (typeof result !== "number") {
|
||||||
|
throw new Error(`Invalid expiration format: ${exp}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMinExpiresIn = (exp1: string | number, exp2: string | number): string | number => {
|
||||||
|
const ms1 = convertToMilliseconds(exp1);
|
||||||
|
const ms2 = convertToMilliseconds(exp2);
|
||||||
|
|
||||||
|
return ms1 <= ms2 ? exp1 : exp2;
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import jwt from "jsonwebtoken";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { getMinExpiresIn } from "@app/lib/fn";
|
||||||
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
@@ -79,6 +80,18 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||||
|
if (decodedToken.organizationId) {
|
||||||
|
const org = await server.services.org.findOrganizationById(
|
||||||
|
decodedToken.userId,
|
||||||
|
decodedToken.organizationId,
|
||||||
|
decodedToken.authMethod,
|
||||||
|
decodedToken.organizationId
|
||||||
|
);
|
||||||
|
if (org && org.userTokenExpiration) {
|
||||||
|
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{
|
{
|
||||||
@@ -92,7 +105,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
mfaMethod: decodedToken.mfaMethod
|
mfaMethod: decodedToken.mfaMethod
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
{ expiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { token, organizationId: decodedToken.organizationId };
|
return { token, organizationId: decodedToken.organizationId };
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -263,7 +264,18 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
enforceMfa: z.boolean().optional(),
|
enforceMfa: z.boolean().optional(),
|
||||||
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
|
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
|
||||||
allowSecretSharingOutsideOrganization: z.boolean().optional(),
|
allowSecretSharingOutsideOrganization: z.boolean().optional(),
|
||||||
bypassOrgAuthEnabled: z.boolean().optional()
|
bypassOrgAuthEnabled: z.boolean().optional(),
|
||||||
|
userTokenExpiration: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => new RE2(/^\d+[mhdw]$/).test(val), "Must be a number followed by m, h, d, or w")
|
||||||
|
.refine(
|
||||||
|
(val) => {
|
||||||
|
const numericPart = val.slice(0, -1);
|
||||||
|
return parseInt(numericPart, 10) >= 1;
|
||||||
|
},
|
||||||
|
{ message: "Duration value must be at least 1" }
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
|||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
@@ -143,6 +143,17 @@ export const authLoginServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (!tokenSession) throw new Error("Failed to create token");
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
|
|
||||||
|
let tokenSessionExpiresIn: string | number = cfg.JWT_AUTH_LIFETIME;
|
||||||
|
let refreshTokenExpiresIn: string | number = cfg.JWT_REFRESH_LIFETIME;
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
const org = await orgDAL.findById(organizationId);
|
||||||
|
if (org && org.userTokenExpiration) {
|
||||||
|
tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||||
|
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authMethod,
|
authMethod,
|
||||||
@@ -155,7 +166,7 @@ export const authLoginServiceFactory = ({
|
|||||||
mfaMethod
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: tokenSessionExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshToken = jwt.sign(
|
const refreshToken = jwt.sign(
|
||||||
@@ -170,7 +181,7 @@ export const authLoginServiceFactory = ({
|
|||||||
mfaMethod
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
{ expiresIn: refreshTokenExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { access: accessToken, refresh: refreshToken };
|
return { access: accessToken, refresh: refreshToken };
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { getMinExpiresIn } from "@app/lib/fn";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -46,7 +47,7 @@ type TAuthSignupDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
@@ -320,6 +321,17 @@ export const authSignupServiceFactory = ({
|
|||||||
projectBotDAL
|
projectBotDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let tokenSessionExpiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||||
|
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId);
|
||||||
|
if (org && org.userTokenExpiration) {
|
||||||
|
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||||
|
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tokenSession = await tokenService.getUserTokenSession({
|
const tokenSession = await tokenService.getUserTokenSession({
|
||||||
userAgent,
|
userAgent,
|
||||||
ip,
|
ip,
|
||||||
@@ -337,7 +349,7 @@ export const authSignupServiceFactory = ({
|
|||||||
organizationId
|
organizationId
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: tokenSessionExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshToken = jwt.sign(
|
const refreshToken = jwt.sign(
|
||||||
@@ -350,7 +362,7 @@ export const authSignupServiceFactory = ({
|
|||||||
organizationId
|
organizationId
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
|
{ expiresIn: refreshTokenExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { user: updateduser.info, accessToken, refreshToken, organizationId };
|
return { user: updateduser.info, accessToken, refreshToken, organizationId };
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
|||||||
shouldUseNewPrivilegeSystem: true,
|
shouldUseNewPrivilegeSystem: true,
|
||||||
privilegeUpgradeInitiatedByUsername: true,
|
privilegeUpgradeInitiatedByUsername: true,
|
||||||
privilegeUpgradeInitiatedAt: true,
|
privilegeUpgradeInitiatedAt: true,
|
||||||
bypassOrgAuthEnabled: true
|
bypassOrgAuthEnabled: true,
|
||||||
|
userTokenExpiration: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -170,8 +170,12 @@ export const orgServiceFactory = ({
|
|||||||
actorOrgId: string | undefined
|
actorOrgId: string | undefined
|
||||||
) => {
|
) => {
|
||||||
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
const appCfg = getConfig();
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
|
if (!org.userTokenExpiration) {
|
||||||
|
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
|
||||||
|
}
|
||||||
return org;
|
return org;
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
@@ -350,7 +354,8 @@ export const orgServiceFactory = ({
|
|||||||
enforceMfa,
|
enforceMfa,
|
||||||
selectedMfaMethod,
|
selectedMfaMethod,
|
||||||
allowSecretSharingOutsideOrganization,
|
allowSecretSharingOutsideOrganization,
|
||||||
bypassOrgAuthEnabled
|
bypassOrgAuthEnabled,
|
||||||
|
userTokenExpiration
|
||||||
}
|
}
|
||||||
}: TUpdateOrgDTO) => {
|
}: TUpdateOrgDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -451,7 +456,8 @@ export const orgServiceFactory = ({
|
|||||||
enforceMfa,
|
enforceMfa,
|
||||||
selectedMfaMethod,
|
selectedMfaMethod,
|
||||||
allowSecretSharingOutsideOrganization,
|
allowSecretSharingOutsideOrganization,
|
||||||
bypassOrgAuthEnabled
|
bypassOrgAuthEnabled,
|
||||||
|
userTokenExpiration
|
||||||
});
|
});
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
return org;
|
return org;
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
|
|||||||
selectedMfaMethod: MfaMethod;
|
selectedMfaMethod: MfaMethod;
|
||||||
allowSecretSharingOutsideOrganization: boolean;
|
allowSecretSharingOutsideOrganization: boolean;
|
||||||
bypassOrgAuthEnabled: boolean;
|
bypassOrgAuthEnabled: boolean;
|
||||||
|
userTokenExpiration: string;
|
||||||
}>;
|
}>;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ The **Settings** page lets you manage information about your organization includ
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
You can adjust the maximum time a user token will remain valid for your organization. After this period, users will be required to re-authenticate. This helps improve security by enforcing regular sign-ins.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
## Access Control
|
## Access Control
|
||||||
|
|
||||||
The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
|
The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 352 KiB |
@@ -111,7 +111,8 @@ export const useUpdateOrg = () => {
|
|||||||
enforceMfa,
|
enforceMfa,
|
||||||
selectedMfaMethod,
|
selectedMfaMethod,
|
||||||
allowSecretSharingOutsideOrganization,
|
allowSecretSharingOutsideOrganization,
|
||||||
bypassOrgAuthEnabled
|
bypassOrgAuthEnabled,
|
||||||
|
userTokenExpiration
|
||||||
}) => {
|
}) => {
|
||||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||||
name,
|
name,
|
||||||
@@ -122,7 +123,8 @@ export const useUpdateOrg = () => {
|
|||||||
enforceMfa,
|
enforceMfa,
|
||||||
selectedMfaMethod,
|
selectedMfaMethod,
|
||||||
allowSecretSharingOutsideOrganization,
|
allowSecretSharingOutsideOrganization,
|
||||||
bypassOrgAuthEnabled
|
bypassOrgAuthEnabled,
|
||||||
|
userTokenExpiration
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type Organization = {
|
|||||||
selectedMfaMethod?: MfaMethod;
|
selectedMfaMethod?: MfaMethod;
|
||||||
shouldUseNewPrivilegeSystem: boolean;
|
shouldUseNewPrivilegeSystem: boolean;
|
||||||
allowSecretSharingOutsideOrganization?: boolean;
|
allowSecretSharingOutsideOrganization?: boolean;
|
||||||
|
userTokenExpiration?: string;
|
||||||
userRole: string;
|
userRole: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ export type UpdateOrgDTO = {
|
|||||||
selectedMfaMethod?: MfaMethod;
|
selectedMfaMethod?: MfaMethod;
|
||||||
allowSecretSharingOutsideOrganization?: boolean;
|
allowSecretSharingOutsideOrganization?: boolean;
|
||||||
bypassOrgAuthEnabled?: boolean;
|
bypassOrgAuthEnabled?: boolean;
|
||||||
|
userTokenExpiration?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BillingDetails = {
|
export type BillingDetails = {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { OrgLDAPSection } from "./OrgLDAPSection";
|
|||||||
import { OrgOIDCSection } from "./OrgOIDCSection";
|
import { OrgOIDCSection } from "./OrgOIDCSection";
|
||||||
import { OrgScimSection } from "./OrgSCIMSection";
|
import { OrgScimSection } from "./OrgSCIMSection";
|
||||||
import { OrgSSOSection } from "./OrgSSOSection";
|
import { OrgSSOSection } from "./OrgSSOSection";
|
||||||
|
import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection";
|
||||||
import { SSOModal } from "./SSOModal";
|
import { SSOModal } from "./SSOModal";
|
||||||
|
|
||||||
export const OrgAuthTab = withPermission(
|
export const OrgAuthTab = withPermission(
|
||||||
@@ -167,6 +168,7 @@ export const OrgAuthTab = withPermission(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OrgGenericAuthSection />
|
<OrgGenericAuthSection />
|
||||||
|
<OrgUserAccessTokenLimitSection />
|
||||||
{shouldShowCreateIdentityProviderView ? (
|
{shouldShowCreateIdentityProviderView ? (
|
||||||
createIdentityProviderView
|
createIdentityProviderView
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
|
import { useUpdateOrg } from "@app/hooks/api";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
expirationValue: z.number().min(1, "Value must be at least 1"),
|
||||||
|
expirationUnit: z.enum(["m", "h", "d", "w"], {
|
||||||
|
invalid_type_error: "Please select a valid time unit"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
// Function to parse duration string like "30d" into value and unit
|
||||||
|
const parseDuration = (duration: string): { value: number; unit: string } => {
|
||||||
|
const match = duration.match(/^(\d+)([mhdw])$/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
value: parseInt(match[1], 10),
|
||||||
|
unit: match[2]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Default to 30 days if invalid format
|
||||||
|
return { value: 30, unit: "d" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to format value and unit back to duration string
|
||||||
|
const formatDuration = (value: number, unit: string): string => {
|
||||||
|
return `${value}${unit}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OrgUserAccessTokenLimitSection = () => {
|
||||||
|
const { mutateAsync: updateUserTokenExpiration } = useUpdateOrg();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
// Parse the current duration or use default
|
||||||
|
const currentDuration = parseDuration(currentOrg?.userTokenExpiration || "30d");
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting, isDirty },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
expirationValue: currentDuration.value,
|
||||||
|
expirationUnit: currentDuration.unit as "m" | "h" | "d" | "w"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentOrg) return null;
|
||||||
|
|
||||||
|
const handleUserTokenExpirationSubmit = async (formData: TForm) => {
|
||||||
|
try {
|
||||||
|
const userTokenExpiration = formatDuration(formData.expirationValue, formData.expirationUnit);
|
||||||
|
|
||||||
|
await updateUserTokenExpiration({
|
||||||
|
userTokenExpiration,
|
||||||
|
orgId: currentOrg.id
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated user token expiration",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed updating user token expiration",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Units for the dropdown with readable labels
|
||||||
|
const timeUnits = [
|
||||||
|
{ value: "m", label: "Minutes" },
|
||||||
|
{ value: "h", label: "Hours" },
|
||||||
|
{ value: "d", label: "Days" },
|
||||||
|
{ value: "w", label: "Weeks" }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<p className="text-xl font-semibold">User Token Expiration</p>
|
||||||
|
</div>
|
||||||
|
<p className="mb-4 mt-2 text-sm text-gray-400">
|
||||||
|
This defines the maximum time a user token will be valid. After this time, the user will
|
||||||
|
need to re-authenticate.
|
||||||
|
</p>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<form onSubmit={handleSubmit(handleUserTokenExpirationSubmit)} autoComplete="off">
|
||||||
|
<div className="flex max-w-md gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expirationValue"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Expiration value"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expirationUnit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Time unit"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
className="pr-2"
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
placeholder="Select time unit"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
{timeUnits.map(({ value, label }) => (
|
||||||
|
<SelectItem
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">{label}</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
disabled={!isDirty}
|
||||||
|
className="mt-4"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user