mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 20:26:05 +00:00
fix: review changes
This commit is contained in:
@@ -43,15 +43,10 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: authRateLimit
|
rateLimit: authRateLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z
|
body: z.object({
|
||||||
.object({
|
organizationId: z.string().trim(),
|
||||||
organizationId: z.string().trim().optional(),
|
userAgent: z.enum(["cli"]).optional()
|
||||||
subOrganizationId: z.string().trim().optional(),
|
}),
|
||||||
userAgent: z.enum(["cli"]).optional()
|
|
||||||
})
|
|
||||||
.refine((body) => Boolean(body.organizationId || body.subOrganizationId), {
|
|
||||||
message: "organizationId or subOrganizationId is required"
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
@@ -62,25 +57,13 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
let tokens;
|
|
||||||
|
|
||||||
const targetOrgId = req.body.subOrganizationId ?? req.body.organizationId ?? "";
|
const tokens = await server.services.login.selectOrganization({
|
||||||
|
userAgent: req.body.userAgent ?? req.headers["user-agent"],
|
||||||
if (req.body.subOrganizationId) {
|
authJwtToken: req.headers.authorization,
|
||||||
tokens = await server.services.login.selectSubOrganization({
|
organizationId: req.body.organizationId,
|
||||||
userAgent: req.body.userAgent ?? req.headers["user-agent"],
|
ipAddress: req.realIp
|
||||||
authJwtToken: req.headers.authorization,
|
});
|
||||||
subOrganizationId: req.body.subOrganizationId,
|
|
||||||
ipAddress: req.realIp
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tokens = await server.services.login.selectOrganization({
|
|
||||||
userAgent: req.body.userAgent ?? req.headers["user-agent"],
|
|
||||||
authJwtToken: req.headers.authorization,
|
|
||||||
organizationId: req.body.organizationId as string,
|
|
||||||
ipAddress: req.realIp
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokens.isMfaEnabled) {
|
if (tokens.isMfaEnabled) {
|
||||||
return {
|
return {
|
||||||
@@ -93,7 +76,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
|
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
|
||||||
if (githubOauthAccessToken) {
|
if (githubOauthAccessToken) {
|
||||||
await server.services.githubOrgSync
|
await server.services.githubOrgSync
|
||||||
.syncUserGroups(targetOrgId, tokens.user.userId, githubOauthAccessToken)
|
.syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
|
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
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,
|
||||||
|
NotFoundError,
|
||||||
|
UnauthorizedError
|
||||||
|
} from "@app/lib/errors";
|
||||||
import { getMinExpiresIn, 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 { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
@@ -530,25 +536,77 @@ export const authLoginServiceFactory = ({
|
|||||||
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
|
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
|
||||||
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
|
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
|
||||||
|
|
||||||
// Check if the user actually has access to the specified organization.
|
|
||||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
|
||||||
|
|
||||||
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
|
|
||||||
|
|
||||||
const selectedOrg = await orgDAL.findById(organizationId);
|
const selectedOrg = await orgDAL.findById(organizationId);
|
||||||
|
if (!selectedOrg) {
|
||||||
|
throw new NotFoundError({ message: `Organization with ID '${organizationId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedOrgMembership) {
|
const isSubOrganization = Boolean(selectedOrg.rootOrgId && selectedOrg.id !== selectedOrg.rootOrgId);
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
let rootOrg = selectedOrg;
|
||||||
|
let membershipRole;
|
||||||
|
|
||||||
|
if (isSubOrganization) {
|
||||||
|
if (!selectedOrg.rootOrgId) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid sub-organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rootOrg = await orgDAL.findById(selectedOrg.rootOrgId);
|
||||||
|
if (!rootOrg) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid root organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user membership in the sub-organization
|
||||||
|
const orgMembership = await membershipUserDAL.findOne({
|
||||||
|
actorUserId: user.id,
|
||||||
|
scopeOrgId: organizationId,
|
||||||
|
scope: AccessScope.Organization,
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orgMembership) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: `User does not have access to the sub-organization named ${selectedOrg.name}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user membership in the root organization
|
||||||
|
const rootOrgMembership = await membershipUserDAL.findOne({
|
||||||
|
actorUserId: user.id,
|
||||||
|
scopeOrgId: rootOrg.id,
|
||||||
|
scope: AccessScope.Organization,
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!rootOrgMembership) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "User does not have access to the root organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
membershipRole = (await membershipRoleDAL.findOne({ membershipId: orgMembership.id })).role;
|
||||||
|
} else {
|
||||||
|
// For root organizations, check membership using the existing method
|
||||||
|
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||||
|
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||||
|
|
||||||
|
if (!selectedOrgMembership) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: `User does not have access to the organization named ${selectedOrg.name}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
membershipRole = selectedOrgMembership.userRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if authEnforced is true and the current auth method is not an enforced method
|
|
||||||
if (
|
if (
|
||||||
selectedOrg.authEnforced &&
|
selectedOrg.authEnforced &&
|
||||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||||
decodedToken.authMethod !== AuthMethod.OIDC &&
|
decodedToken.authMethod !== AuthMethod.OIDC &&
|
||||||
!(selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin)
|
!(selectedOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin)
|
||||||
) {
|
) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Login with the auth method required by your organization."
|
message: "Login with the auth method required by your organization."
|
||||||
@@ -556,7 +614,7 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
||||||
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin;
|
const canBypass = selectedOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin;
|
||||||
|
|
||||||
if (!canBypass) {
|
if (!canBypass) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
@@ -607,7 +665,8 @@ export const authLoginServiceFactory = ({
|
|||||||
user,
|
user,
|
||||||
userAgent,
|
userAgent,
|
||||||
ip: ipAddress,
|
ip: ipAddress,
|
||||||
organizationId,
|
organizationId: isSubOrganization ? rootOrg.id : organizationId,
|
||||||
|
subOrganizationId: isSubOrganization ? organizationId : undefined,
|
||||||
isMfaVerified: decodedToken.isMfaVerified,
|
isMfaVerified: decodedToken.isMfaVerified,
|
||||||
mfaMethod: decodedToken.mfaMethod
|
mfaMethod: decodedToken.mfaMethod
|
||||||
});
|
});
|
||||||
@@ -675,205 +734,55 @@ export const authLoginServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await auditLogService.createAuditLog({
|
// Create audit log for organization selection
|
||||||
orgId: organizationId,
|
if (isSubOrganization) {
|
||||||
ipAddress,
|
await auditLogService.createAuditLog({
|
||||||
userAgent,
|
orgId: organizationId,
|
||||||
userAgentType: getUserAgentType(userAgent),
|
ipAddress,
|
||||||
actor: {
|
userAgent,
|
||||||
type: ActorType.USER,
|
userAgentType: getUserAgentType(userAgent),
|
||||||
metadata: {
|
actor: {
|
||||||
email: user.email,
|
type: ActorType.USER,
|
||||||
userId: user.id,
|
metadata: {
|
||||||
username: user.username,
|
email: user.email,
|
||||||
authMethod: decodedToken.authMethod
|
userId: user.id,
|
||||||
}
|
username: user.username,
|
||||||
},
|
authMethod: decodedToken.authMethod
|
||||||
event: {
|
}
|
||||||
type: EventType.SELECT_ORGANIZATION,
|
|
||||||
metadata: {
|
|
||||||
organizationId,
|
|
||||||
organizationName: selectedOrg.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...tokens,
|
|
||||||
user,
|
|
||||||
isMfaEnabled: false
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectSubOrganization = async ({
|
|
||||||
userAgent,
|
|
||||||
authJwtToken,
|
|
||||||
ipAddress,
|
|
||||||
subOrganizationId
|
|
||||||
}: {
|
|
||||||
userAgent: string | undefined;
|
|
||||||
authJwtToken: string | undefined;
|
|
||||||
ipAddress: string;
|
|
||||||
subOrganizationId: string;
|
|
||||||
}) => {
|
|
||||||
const cfg = getConfig();
|
|
||||||
|
|
||||||
if (!authJwtToken) throw new UnauthorizedError({ name: "Authorization header is required" });
|
|
||||||
if (!userAgent) throw new UnauthorizedError({ name: "User-Agent header is required" });
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
authJwtToken = authJwtToken.replace("Bearer ", "");
|
|
||||||
|
|
||||||
const decodedToken = crypto.jwt().verify(authJwtToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload;
|
|
||||||
|
|
||||||
if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" });
|
|
||||||
|
|
||||||
const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId);
|
|
||||||
if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" });
|
|
||||||
|
|
||||||
// Check user membership in the sub-organization
|
|
||||||
const userSubOrgMembership = await membershipUserDAL.findOne({
|
|
||||||
actorUserId: user.id,
|
|
||||||
scopeOrgId: subOrganizationId,
|
|
||||||
scope: AccessScope.Organization,
|
|
||||||
status: OrgMembershipStatus.Accepted
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch the sub-organization
|
|
||||||
const subOrg = await orgDAL.findById(subOrganizationId);
|
|
||||||
|
|
||||||
if (!userSubOrgMembership) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: `User does not have access to the sub-organization named ${subOrg.name}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subOrg.rootOrgId) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Invalid sub-organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootOrg = await orgDAL.findById(subOrg.rootOrgId);
|
|
||||||
|
|
||||||
if (!rootOrg) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Invalid root organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootOrgMembership = await membershipUserDAL.findOne({
|
|
||||||
actorUserId: user.id,
|
|
||||||
scopeOrgId: rootOrg.id,
|
|
||||||
scope: AccessScope.Organization,
|
|
||||||
status: OrgMembershipStatus.Accepted
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!rootOrgMembership) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "User does not have access to the root organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const subOrgmembershipRole = await membershipRoleDAL.findOne({ membershipId: userSubOrgMembership.id });
|
|
||||||
|
|
||||||
// Check if authEnforced is true and the current auth method is not an enforced method
|
|
||||||
if (
|
|
||||||
subOrg.authEnforced &&
|
|
||||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
|
||||||
decodedToken.authMethod !== AuthMethod.OIDC &&
|
|
||||||
!(subOrg.bypassOrgAuthEnabled && subOrgmembershipRole.role === OrgMembershipRole.Admin)
|
|
||||||
) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Login with the auth method required by your organization."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
|
||||||
const canBypass = subOrg.bypassOrgAuthEnabled && subOrgmembershipRole.role === OrgMembershipRole.Admin;
|
|
||||||
|
|
||||||
if (!canBypass) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "Google SSO is enforced for this organization. Please use Google SSO to login.",
|
|
||||||
error: "GoogleSsoEnforced"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decodedToken.authMethod === AuthMethod.GOOGLE) {
|
|
||||||
await orgDAL.updateById(subOrg.id, {
|
|
||||||
googleSsoAuthLastUsed: new Date()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check MFA requirements for the sub-organization
|
|
||||||
const shouldCheckMfa = subOrg.enforceMfa || user.isMfaEnabled;
|
|
||||||
const orgMfaMethod = subOrg.enforceMfa ? (subOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
|
||||||
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
|
||||||
const mfaMethod = orgMfaMethod ?? userMfaMethod;
|
|
||||||
|
|
||||||
if (shouldCheckMfa && (!decodedToken.isMfaVerified || decodedToken.mfaMethod !== mfaMethod)) {
|
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
|
||||||
|
|
||||||
const mfaToken = crypto.jwt().sign(
|
|
||||||
{
|
|
||||||
authMethod: decodedToken.authMethod,
|
|
||||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
|
||||||
userId: user.id
|
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
event: {
|
||||||
{
|
type: EventType.SELECT_SUB_ORGANIZATION,
|
||||||
expiresIn: cfg.JWT_MFA_LIFETIME
|
metadata: {
|
||||||
|
organizationId,
|
||||||
|
organizationName: selectedOrg.name,
|
||||||
|
rootOrganizationId: rootOrg.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
} else {
|
||||||
if (mfaMethod === MfaMethod.EMAIL && user.email) {
|
await auditLogService.createAuditLog({
|
||||||
await sendUserMfaCode({
|
orgId: organizationId,
|
||||||
userId: user.id,
|
ipAddress,
|
||||||
email: user.email
|
userAgent,
|
||||||
});
|
userAgentType: getUserAgentType(userAgent),
|
||||||
}
|
actor: {
|
||||||
|
type: ActorType.USER,
|
||||||
return { isMfaEnabled: true, mfa: mfaToken, mfaMethod } as const;
|
metadata: {
|
||||||
|
email: user.email,
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
authMethod: decodedToken.authMethod
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: EventType.SELECT_ORGANIZATION,
|
||||||
|
metadata: {
|
||||||
|
organizationId,
|
||||||
|
organizationName: selectedOrg.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate tokens scoped to the sub-organization
|
|
||||||
const tokens = await generateUserTokens({
|
|
||||||
authMethod: decodedToken.authMethod,
|
|
||||||
user,
|
|
||||||
userAgent,
|
|
||||||
ip: ipAddress,
|
|
||||||
organizationId: rootOrg.id,
|
|
||||||
subOrganizationId,
|
|
||||||
isMfaVerified: decodedToken.isMfaVerified,
|
|
||||||
mfaMethod: decodedToken.mfaMethod
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create audit log for sub-organization selection
|
|
||||||
await auditLogService.createAuditLog({
|
|
||||||
orgId: subOrganizationId,
|
|
||||||
ipAddress,
|
|
||||||
userAgent,
|
|
||||||
userAgentType: getUserAgentType(userAgent),
|
|
||||||
actor: {
|
|
||||||
type: ActorType.USER,
|
|
||||||
metadata: {
|
|
||||||
email: user.email,
|
|
||||||
userId: user.id,
|
|
||||||
username: user.username,
|
|
||||||
authMethod: decodedToken.authMethod
|
|
||||||
}
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
type: EventType.SELECT_SUB_ORGANIZATION,
|
|
||||||
metadata: {
|
|
||||||
organizationId: subOrganizationId,
|
|
||||||
organizationName: subOrg.name,
|
|
||||||
rootOrganizationId: subOrg.rootOrgId ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...tokens,
|
...tokens,
|
||||||
user,
|
user,
|
||||||
@@ -1314,7 +1223,6 @@ export const authLoginServiceFactory = ({
|
|||||||
resendMfaToken,
|
resendMfaToken,
|
||||||
verifyMfaToken,
|
verifyMfaToken,
|
||||||
selectOrganization,
|
selectOrganization,
|
||||||
selectSubOrganization,
|
|
||||||
generateUserTokens,
|
generateUserTokens,
|
||||||
login
|
login
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,15 +58,10 @@ export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SelectOrganizationParams =
|
export type SelectOrganizationParams = {
|
||||||
| {
|
organizationId: string;
|
||||||
organizationId: string;
|
userAgent?: UserAgentType;
|
||||||
userAgent?: UserAgentType;
|
};
|
||||||
}
|
|
||||||
| {
|
|
||||||
subOrganizationId: string;
|
|
||||||
userAgent?: UserAgentType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const selectOrganization = async (data: SelectOrganizationParams) => {
|
export const selectOrganization = async (data: SelectOrganizationParams) => {
|
||||||
const { data: res } = await apiRequest.post<{
|
const { data: res } = await apiRequest.post<{
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ export const useCreateSubOrganization = () => {
|
|||||||
mutationFn: async (dto: TCreateSubOrganizationDTO) => {
|
mutationFn: async (dto: TCreateSubOrganizationDTO) => {
|
||||||
const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
|
const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
|
||||||
"/api/v1/sub-organizations",
|
"/api/v1/sub-organizations",
|
||||||
dto,
|
dto
|
||||||
{
|
|
||||||
headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -62,11 +62,7 @@ import {
|
|||||||
useGetOrgTrialUrl,
|
useGetOrgTrialUrl,
|
||||||
useLogoutUser
|
useLogoutUser
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import {
|
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
authKeys,
|
|
||||||
selectOrganization,
|
|
||||||
type SelectOrganizationParams
|
|
||||||
} from "@app/hooks/api/auth/queries";
|
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
||||||
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
||||||
@@ -197,26 +193,18 @@ export const Navbar = () => {
|
|||||||
|
|
||||||
const handleOrgSelection = async ({
|
const handleOrgSelection = async ({
|
||||||
organizationId,
|
organizationId,
|
||||||
subOrganizationId,
|
|
||||||
navigateTo,
|
navigateTo,
|
||||||
onSuccess
|
onSuccess
|
||||||
}: {
|
}: {
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
subOrganizationId?: string;
|
|
||||||
navigateTo?: string;
|
navigateTo?: string;
|
||||||
onSuccess?: () => void | Promise<void>;
|
onSuccess?: () => void | Promise<void>;
|
||||||
}) => {
|
}) => {
|
||||||
if (!organizationId && !subOrganizationId) return;
|
if (!organizationId) return;
|
||||||
|
|
||||||
const targetId = subOrganizationId ?? organizationId;
|
if (organizationId === currentOrg.id) return;
|
||||||
|
|
||||||
if (targetId === currentOrg.id) return;
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
|
||||||
|
|
||||||
const selectionPayload: SelectOrganizationParams = subOrganizationId
|
|
||||||
? { subOrganizationId }
|
|
||||||
: { organizationId: organizationId as string };
|
|
||||||
|
|
||||||
const { token, isMfaEnabled, mfaMethod } = await selectOrganization(selectionPayload);
|
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(token);
|
SecurityClient.setMfaToken(token);
|
||||||
@@ -225,7 +213,7 @@ export const Navbar = () => {
|
|||||||
}
|
}
|
||||||
toggleShowMfa.on();
|
toggleShowMfa.on();
|
||||||
setMfaSuccessCallback(() => async () => {
|
setMfaSuccessCallback(() => async () => {
|
||||||
await handleOrgSelection({ organizationId, subOrganizationId, onSuccess });
|
await handleOrgSelection({ organizationId, onSuccess });
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -234,11 +222,12 @@ export const Navbar = () => {
|
|||||||
SecurityClient.setProviderAuthToken("");
|
SecurityClient.setProviderAuthToken("");
|
||||||
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||||
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||||
|
|
||||||
await router.invalidate();
|
|
||||||
await navigateUserToOrg({ navigate, organizationId: targetId, navigateTo });
|
|
||||||
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
||||||
|
|
||||||
|
await queryClient.refetchQueries({ queryKey: authKeys.getAuthToken });
|
||||||
|
|
||||||
|
await navigateUserToOrg({ navigate, organizationId, navigateTo });
|
||||||
|
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
await onSuccess();
|
await onSuccess();
|
||||||
}
|
}
|
||||||
@@ -387,14 +376,17 @@ export const Navbar = () => {
|
|||||||
<button
|
<button
|
||||||
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
|
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (isSubOrganization) {
|
if (isSubOrganization) {
|
||||||
handleOrgSelection({ organizationId: currentOrg.rootOrgId as string });
|
await handleOrgSelection({
|
||||||
|
organizationId: currentOrg.rootOrgId as string
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigate({
|
||||||
|
to: "/organizations/$orgId/projects",
|
||||||
|
params: { orgId: currentOrg.id }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
navigate({
|
|
||||||
to: "/organizations/$orgId/projects",
|
|
||||||
params: { orgId: currentOrg.id }
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
|
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
|
||||||
@@ -471,7 +463,7 @@ export const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
{subOrganizations.map((subOrg) => (
|
{subOrganizations.map((subOrg) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })}
|
onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
|
||||||
className="cursor-pointer font-normal"
|
className="cursor-pointer font-normal"
|
||||||
key={subOrg.id}
|
key={subOrg.id}
|
||||||
>
|
>
|
||||||
@@ -486,18 +478,16 @@ export const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
{Boolean(subOrganizations.length && !isSubOrganization) && (
|
{Boolean(subOrganizations.length) && (
|
||||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
)}
|
)}
|
||||||
{!isSubOrganization && (
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem
|
className="cursor-pointer"
|
||||||
className="cursor-pointer"
|
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
onClick={() => setShowSubOrgForm(true)}
|
||||||
onClick={() => setShowSubOrgForm(true)}
|
>
|
||||||
>
|
New Sub-Organization
|
||||||
New Sub-Organization
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
|
||||||
)}{" "}
|
|
||||||
</DropdownSubMenuContent>
|
</DropdownSubMenuContent>
|
||||||
</DropdownSubMenu>
|
</DropdownSubMenu>
|
||||||
);
|
);
|
||||||
@@ -590,7 +580,7 @@ export const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
{subOrganizations.map((subOrg) => (
|
{subOrganizations.map((subOrg) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleOrgSelection({ subOrganizationId: subOrg.id })}
|
onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
|
||||||
className="cursor-pointer font-normal"
|
className="cursor-pointer font-normal"
|
||||||
key={subOrg.id}
|
key={subOrg.id}
|
||||||
>
|
>
|
||||||
@@ -602,18 +592,16 @@ export const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
{Boolean(subOrganizations.length && !isSubOrganization) && (
|
{Boolean(subOrganizations.length) && (
|
||||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
)}
|
)}
|
||||||
{!isSubOrganization && (
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem
|
className="cursor-pointer"
|
||||||
className="cursor-pointer"
|
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
onClick={() => setShowSubOrgForm(true)}
|
||||||
onClick={() => setShowSubOrgForm(true)}
|
>
|
||||||
>
|
New Sub-Organization
|
||||||
New Sub-Organization
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+12
-1
@@ -7,6 +7,7 @@ import { z } from "zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
import { projectKeys, subOrganizationsQuery, useCreateSubOrganization } from "@app/hooks/api";
|
import { projectKeys, subOrganizationsQuery, useCreateSubOrganization } from "@app/hooks/api";
|
||||||
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
|
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
@@ -23,6 +24,7 @@ const AddOrgSchema = z.object({
|
|||||||
type FormData = z.infer<typeof AddOrgSchema>;
|
type FormData = z.infer<typeof AddOrgSchema>;
|
||||||
|
|
||||||
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
||||||
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
const createSubOrg = useCreateSubOrganization();
|
const createSubOrg = useCreateSubOrganization();
|
||||||
const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true });
|
const subOrgQuery = subOrganizationsQuery.list({ limit: 500, isAccessible: true });
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -42,6 +44,15 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const onSubmit = async ({ name }: FormData) => {
|
const onSubmit = async ({ name }: FormData) => {
|
||||||
|
if (isSubOrganization && currentOrg.rootOrgId) {
|
||||||
|
const { token } = await selectOrganization({
|
||||||
|
organizationId: currentOrg.rootOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
SecurityClient.setToken(token);
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
}
|
||||||
|
|
||||||
const { organization } = await createSubOrg.mutateAsync({
|
const { organization } = await createSubOrg.mutateAsync({
|
||||||
name
|
name
|
||||||
});
|
});
|
||||||
@@ -53,7 +64,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
|||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
const { token } = await selectOrganization({
|
const { token } = await selectOrganization({
|
||||||
subOrganizationId: organization.id
|
organizationId: organization.id
|
||||||
});
|
});
|
||||||
|
|
||||||
SecurityClient.setToken(token);
|
SecurityClient.setToken(token);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const tabs = [
|
|||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
>
|
>
|
||||||
<InfoIcon size={12} /> Looking for organization settings?
|
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
|
||||||
|
settings?
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const tabs = [
|
|||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
@@ -39,7 +39,8 @@ export const SettingsPage = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
>
|
>
|
||||||
<InfoIcon size={12} /> Looking for organization settings?
|
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
|
||||||
|
settings?
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ export const Route = createFileRoute("/_authenticate")({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return { organizationId: data.organizationId as string, isAuthenticated: true, user };
|
const isSubOrganization = !!data.subOrganizationId;
|
||||||
|
return {
|
||||||
|
organizationId: isSubOrganization ? data.subOrganizationId : (data.organizationId as string),
|
||||||
|
isAuthenticated: true,
|
||||||
|
user
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
|
import { authKeys, fetchAuthToken, selectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
|
||||||
|
import { projectKeys } from "@app/hooks/api/projects";
|
||||||
import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
||||||
|
import { subOrganizationsQuery } from "@app/hooks/api/subOrganizations";
|
||||||
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
|
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
|
||||||
|
|
||||||
// Route context to fill in organization's data like details, subscription etc
|
// Route context to fill in organization's data like details, subscription etc
|
||||||
@@ -15,6 +19,33 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details")({
|
|||||||
organizationId = context.organizationId!;
|
organizationId = context.organizationId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((params as { orgId?: string })?.orgId && context.organizationId) {
|
||||||
|
const urlOrgId = (params as { orgId: string }).orgId;
|
||||||
|
const currentTokenOrgId = context.organizationId;
|
||||||
|
|
||||||
|
if (urlOrgId !== currentTokenOrgId) {
|
||||||
|
try {
|
||||||
|
const { token, isMfaEnabled } = await selectOrganization({ organizationId: urlOrgId });
|
||||||
|
|
||||||
|
if (!isMfaEnabled && token) {
|
||||||
|
SecurityClient.setToken(token);
|
||||||
|
SecurityClient.setProviderAuthToken("");
|
||||||
|
|
||||||
|
context.queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||||
|
context.queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||||
|
context.queryClient.removeQueries({ queryKey: subOrganizationsQuery.allKey() });
|
||||||
|
|
||||||
|
await context.queryClient.fetchQuery({
|
||||||
|
queryKey: authKeys.getAuthToken,
|
||||||
|
queryFn: fetchAuthToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to automatically exchange token for organization:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await context.queryClient.ensureQueryData({
|
await context.queryClient.ensureQueryData({
|
||||||
queryKey: organizationKeys.getOrgById(organizationId),
|
queryKey: organizationKeys.getOrgById(organizationId),
|
||||||
queryFn: () => fetchOrganizationById(organizationId)
|
queryFn: () => fetchOrganizationById(organizationId)
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Helmet } from "react-helmet";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
|
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
|
||||||
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
@@ -85,7 +86,19 @@ export const AccessManagementPage = () => {
|
|||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`}
|
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`}
|
||||||
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
|
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
|
||||||
/>
|
>
|
||||||
|
{isSubOrganization && (
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/access-management"
|
||||||
|
params={{
|
||||||
|
orgId: currentOrg.rootOrgId ?? ""
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for root organization access control?
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</PageHeader>
|
||||||
{!currentOrg.shouldUseNewPrivilegeSystem && (
|
{!currentOrg.shouldUseNewPrivilegeSystem && (
|
||||||
<div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
<div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
||||||
<div className="mb-1 flex items-center text-sm">
|
<div className="mb-1 flex items-center text-sm">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
@@ -6,7 +8,7 @@ import { useOrganization } from "@app/context";
|
|||||||
import { LogsSection } from "./components";
|
import { LogsSection } from "./components";
|
||||||
|
|
||||||
export const AuditLogsPage = () => {
|
export const AuditLogsPage = () => {
|
||||||
const { isSubOrganization } = useOrganization();
|
const { isSubOrganization, currentOrg } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full bg-bunker-800">
|
<div className="h-full bg-bunker-800">
|
||||||
@@ -21,7 +23,19 @@ export const AuditLogsPage = () => {
|
|||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`}
|
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`}
|
||||||
description="Audit logs for security and compliance teams to monitor information access."
|
description="Audit logs for security and compliance teams to monitor information access."
|
||||||
/>
|
>
|
||||||
|
{isSubOrganization && (
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/audit-logs"
|
||||||
|
params={{
|
||||||
|
orgId: currentOrg.rootOrgId ?? ""
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for root organization audit logs?
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</PageHeader>
|
||||||
<LogsSection pageView />
|
<LogsSection pageView />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
@@ -8,7 +10,7 @@ import { OrgTabGroup } from "./components";
|
|||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isSubOrganization } = useOrganization();
|
const { isSubOrganization, currentOrg } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -21,7 +23,19 @@ export const SettingsPage = () => {
|
|||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
description="Configure organization-wide settings"
|
description="Configure organization-wide settings"
|
||||||
title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"}
|
title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"}
|
||||||
/>
|
>
|
||||||
|
{isSubOrganization && (
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/settings"
|
||||||
|
params={{
|
||||||
|
orgId: currentOrg.rootOrgId ?? ""
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for root organization settings?
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</PageHeader>
|
||||||
<OrgTabGroup />
|
<OrgTabGroup />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/Pr
|
|||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
@@ -31,7 +31,8 @@ export const SettingsPage = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
>
|
>
|
||||||
<InfoIcon size={12} /> Looking for organization settings?
|
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
|
||||||
|
settings?
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const selectedTab = useSearch({
|
const selectedTab = useSearch({
|
||||||
strict: false,
|
strict: false,
|
||||||
@@ -54,7 +54,8 @@ const Page = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
>
|
>
|
||||||
<InfoIcon size={12} /> Looking for organization access control?
|
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization access
|
||||||
|
control?
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { Link } from "@tanstack/react-router";
|
|||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useOrganization, useProject } from "@app/context";
|
||||||
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components";
|
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components";
|
||||||
|
|
||||||
export const AuditLogsPage = () => {
|
export const AuditLogsPage = () => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
const { isSubOrganization } = useOrganization();
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -29,7 +29,8 @@ export const AuditLogsPage = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
>
|
>
|
||||||
<InfoIcon size={12} /> Looking for organization audit logs?
|
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization audit
|
||||||
|
logs?
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<LogsSection pageView project={currentProject} />
|
<LogsSection pageView project={currentProject} />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Link } from "@tanstack/react-router";
|
|||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useOrganization, useProject } from "@app/context";
|
||||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
|
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
|
||||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ import { WorkflowIntegrationTab } from "./components/WorkflowIntegrationSection"
|
|||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isSubOrganization } = useOrganization();
|
||||||
|
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab },
|
{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab },
|
||||||
@@ -55,7 +57,8 @@ export const SettingsPage = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
>
|
>
|
||||||
<InfoIcon size={12} /> Looking for organization settings?
|
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
|
||||||
|
settings?
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab"
|
|||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
>
|
>
|
||||||
<InfoIcon size={12} /> Looking for organization settings?
|
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
|
||||||
|
settings?
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { ProjectSshTab } from "./components/ProjectSshTab";
|
|||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
>
|
>
|
||||||
<InfoIcon size={12} /> Looking for organization settings?
|
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
|
||||||
|
settings?
|
||||||
</Link>
|
</Link>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
||||||
|
|||||||
Reference in New Issue
Block a user