mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-03 09:49:06 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2936cd10b | |||
| 2f013335f9 | |||
| b4f6ae74d7 | |||
| f1711ee0b0 |
+14
-1
@@ -1176,6 +1176,10 @@
|
|||||||
"idpJmespathAboutDescriptionLink": "Learn more about JMESPath",
|
"idpJmespathAboutDescriptionLink": "Learn more about JMESPath",
|
||||||
"idpJmespathLabel": "Identifier Path",
|
"idpJmespathLabel": "Identifier Path",
|
||||||
"idpJmespathLabelDescription": "The path to the user identifier in the ID token",
|
"idpJmespathLabelDescription": "The path to the user identifier in the ID token",
|
||||||
|
"idpIdentifierChangeTitle": "Identifier Path Change Warning",
|
||||||
|
"idpIdentifierChangeDescription": "You are about to change the identifier path. This will affect how existing users are mapped. Users who previously signed in through this identity provider may no longer be recognized as the same users.",
|
||||||
|
"idpIdentifierChangeConfirmMessage": "I confirm",
|
||||||
|
"idpIdentifierChangeWarningText": "This will affect how existing users are mapped",
|
||||||
"idpJmespathEmailPathOptional": "Email Path (Optional)",
|
"idpJmespathEmailPathOptional": "Email Path (Optional)",
|
||||||
"idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token",
|
"idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token",
|
||||||
"idpJmespathNamePathOptional": "Name Path (Optional)",
|
"idpJmespathNamePathOptional": "Name Path (Optional)",
|
||||||
@@ -1420,6 +1424,15 @@
|
|||||||
"logoutError": "Error logging out",
|
"logoutError": "Error logging out",
|
||||||
"signingAs": "Signed in as",
|
"signingAs": "Signed in as",
|
||||||
"serverAdmin": "Server Admin",
|
"serverAdmin": "Server Admin",
|
||||||
|
"promoteServerAdmin": "Promote to Server admin",
|
||||||
|
"promoteServerAdminTitle": "Promote to Server Admin",
|
||||||
|
"promoteServerAdminQuestion": "Are you sure you want to promote {selectedUser} to server admin?",
|
||||||
|
"promoteServerAdminMessage": "Server admins have full access to every organization, user, and setting on this instance.",
|
||||||
|
"promoteServerAdminWarning": "You cannot demote a server admin from this page.",
|
||||||
|
"promoteServerAdminConfirm": "Promote to server admin",
|
||||||
|
"promoteServerAdminSuccess": "User promoted",
|
||||||
|
"promoteServerAdminSuccessDescription": "{selectedUser} is now a server admin.",
|
||||||
|
"promoteServerAdminError": "Failed to promote user",
|
||||||
"managedSelfhosted": "Managed Self-Hosted",
|
"managedSelfhosted": "Managed Self-Hosted",
|
||||||
"otpEnable": "Enable Two-factor",
|
"otpEnable": "Enable Two-factor",
|
||||||
"otpDisable": "Disable Two-factor",
|
"otpDisable": "Disable Two-factor",
|
||||||
@@ -2996,7 +3009,7 @@
|
|||||||
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
|
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
|
||||||
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
|
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
|
||||||
"remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
|
"remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
|
||||||
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will be enforced to connect through this remote exit node.",
|
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will prefer to connect through this remote exit node.",
|
||||||
"remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
|
"remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
|
||||||
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
|
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
|
||||||
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
|
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { and, asc, eq, or } from "drizzle-orm";
|
||||||
|
import { Transaction, User, userOrgs, users } from "@server/db";
|
||||||
|
|
||||||
|
export async function findOrgUserByIdentifier(
|
||||||
|
trx: Transaction,
|
||||||
|
orgId: string,
|
||||||
|
identifier: string
|
||||||
|
): Promise<User | null> {
|
||||||
|
const [match] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
or(eq(users.username, identifier), eq(users.email, identifier)),
|
||||||
|
eq(userOrgs.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(asc(users.dateCreated), asc(users.userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return match?.user ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveOrgUserIds(
|
||||||
|
trx: Transaction,
|
||||||
|
orgId: string,
|
||||||
|
identifiers: string[]
|
||||||
|
): Promise<string[]> {
|
||||||
|
const userIds = new Set<string>();
|
||||||
|
for (const identifier of identifiers) {
|
||||||
|
const user = await findOrgUserByIdentifier(trx, orgId, identifier);
|
||||||
|
if (user) {
|
||||||
|
userIds.add(user.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...userIds];
|
||||||
|
}
|
||||||
@@ -11,15 +11,14 @@ import {
|
|||||||
siteNetworks,
|
siteNetworks,
|
||||||
siteResources,
|
siteResources,
|
||||||
Transaction,
|
Transaction,
|
||||||
userOrgs,
|
|
||||||
users,
|
|
||||||
userSiteResources,
|
userSiteResources,
|
||||||
networks
|
networks
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { sites } from "@server/db";
|
import { sites } from "@server/db";
|
||||||
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
|
import { eq, and, ne, inArray, isNotNull } from "drizzle-orm";
|
||||||
import { Config } from "./types";
|
import { Config } from "./types";
|
||||||
import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
||||||
|
import { resolveOrgUserIds } from "./findOrgUser";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||||
import { getNextAvailableAliasAddress } from "../ip";
|
import { getNextAvailableAliasAddress } from "../ip";
|
||||||
@@ -389,28 +388,22 @@ export async function updatePrivateResources(
|
|||||||
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
||||||
|
|
||||||
if (resourceData.users.length > 0) {
|
if (resourceData.users.length > 0) {
|
||||||
// get userIds from username
|
const userIds = await resolveOrgUserIds(
|
||||||
const usersToUpdate = await trx
|
trx,
|
||||||
.select()
|
orgId,
|
||||||
.from(users)
|
resourceData.users
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
);
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(
|
|
||||||
inArray(users.username, resourceData.users),
|
|
||||||
inArray(users.email, resourceData.users)
|
|
||||||
),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const userIds = usersToUpdate.map((user) => user.user.userId);
|
if (userIds.length > 0) {
|
||||||
|
await trx
|
||||||
await trx
|
.insert(userSiteResources)
|
||||||
.insert(userSiteResources)
|
.values(
|
||||||
.values(
|
userIds.map((userId) => ({
|
||||||
userIds.map((userId) => ({ userId, siteResourceId }))
|
userId,
|
||||||
);
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all admin role IDs for this org to exclude from deletion
|
// Get all admin role IDs for this org to exclude from deletion
|
||||||
@@ -721,28 +714,22 @@ export async function updatePrivateResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resourceData.users.length > 0) {
|
if (resourceData.users.length > 0) {
|
||||||
// get userIds from username
|
const userIds = await resolveOrgUserIds(
|
||||||
const usersToUpdate = await trx
|
trx,
|
||||||
.select()
|
orgId,
|
||||||
.from(users)
|
resourceData.users
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
);
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(
|
|
||||||
inArray(users.username, resourceData.users),
|
|
||||||
inArray(users.email, resourceData.users)
|
|
||||||
),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const userIds = usersToUpdate.map((user) => user.user.userId);
|
if (userIds.length > 0) {
|
||||||
|
await trx
|
||||||
await trx
|
.insert(userSiteResources)
|
||||||
.insert(userSiteResources)
|
.values(
|
||||||
.values(
|
userIds.map((userId) => ({
|
||||||
userIds.map((userId) => ({ userId, siteResourceId }))
|
userId,
|
||||||
);
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceData.machines.length > 0) {
|
if (resourceData.machines.length > 0) {
|
||||||
|
|||||||
@@ -46,11 +46,12 @@ import { encrypt } from "@server/lib/crypto";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||||
import { pickPort } from "@server/routers/target/helpers";
|
import { pickPort } from "@server/routers/target/helpers";
|
||||||
import { and, asc, eq, isNotNull, ne, or } from "drizzle-orm";
|
import { and, asc, eq, isNotNull, ne } from "drizzle-orm";
|
||||||
import { tierMatrix } from "../billing/tierMatrix";
|
import { tierMatrix } from "../billing/tierMatrix";
|
||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
||||||
import { getOrCreateLabelIds, syncResourceLabels } from "./labels";
|
import { getOrCreateLabelIds, syncResourceLabels } from "./labels";
|
||||||
|
import { findOrgUserByIdentifier } from "./findOrgUser";
|
||||||
import { LimitId } from "../billing";
|
import { LimitId } from "../billing";
|
||||||
import { usageService } from "../billing/usageService";
|
import { usageService } from "../billing/usageService";
|
||||||
import { syncInferenceAiConfig } from "./aiProviders";
|
import { syncInferenceAiConfig } from "./aiProviders";
|
||||||
@@ -1563,29 +1564,19 @@ async function syncUserResources(
|
|||||||
.where(eq(userResources.resourceId, resourceId));
|
.where(eq(userResources.resourceId, resourceId));
|
||||||
|
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const [user] = await trx
|
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(eq(users.username, username), eq(users.email, username)),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`User not found: ${username} in org ${orgId}`);
|
throw new Error(`User not found: ${username} in org ${orgId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUserResource = existingUserResources.find(
|
const existingUserResource = existingUserResources.find(
|
||||||
(rr) => rr.userId === user.user.userId
|
(rr) => rr.userId === user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingUserResource) {
|
if (!existingUserResource) {
|
||||||
await trx.insert(userResources).values({
|
await trx.insert(userResources).values({
|
||||||
userId: user.user.userId,
|
userId: user.userId,
|
||||||
resourceId: resourceId
|
resourceId: resourceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1955,29 +1946,19 @@ async function syncUserPolicies(
|
|||||||
.where(eq(userPolicies.resourcePolicyId, policyId));
|
.where(eq(userPolicies.resourcePolicyId, policyId));
|
||||||
|
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const [user] = await trx
|
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(eq(users.username, username), eq(users.email, username)),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`User not found: ${username} in org ${orgId}`);
|
throw new Error(`User not found: ${username} in org ${orgId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUserPolicy = existingUserPoliciesList.find(
|
const existingUserPolicy = existingUserPoliciesList.find(
|
||||||
(up) => up.userId === user.user.userId
|
(up) => up.userId === user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingUserPolicy) {
|
if (!existingUserPolicy) {
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.user.userId,
|
userId: user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
userPolicies,
|
userPolicies,
|
||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, and, or } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { Config, ResourcePolicyData } from "./types";
|
import { Config, ResourcePolicyData } from "./types";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { getUniqueResourcePolicyName } from "@server/db/names";
|
import { getUniqueResourcePolicyName } from "@server/db/names";
|
||||||
@@ -22,6 +22,7 @@ import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
|
|||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "../billing/tierMatrix";
|
import { tierMatrix } from "../billing/tierMatrix";
|
||||||
|
import { findOrgUserByIdentifier } from "./findOrgUser";
|
||||||
|
|
||||||
export type ResourcePoliciesResults = {
|
export type ResourcePoliciesResults = {
|
||||||
resourcePolicyId: number;
|
resourcePolicyId: number;
|
||||||
@@ -466,17 +467,7 @@ async function syncUserPolicies(
|
|||||||
.where(eq(userPolicies.resourcePolicyId, policyId));
|
.where(eq(userPolicies.resourcePolicyId, policyId));
|
||||||
|
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const [user] = await trx
|
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(eq(users.username, username), eq(users.email, username)),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -486,12 +477,12 @@ async function syncUserPolicies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alreadyExists = existingUserPolicies.some(
|
const alreadyExists = existingUserPolicies.some(
|
||||||
(up) => up.userId === user.user.userId
|
(up) => up.userId === user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!alreadyExists) {
|
if (!alreadyExists) {
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.user.userId,
|
userId: user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -536,17 +527,7 @@ async function addUserPolicies(
|
|||||||
trx: Transaction
|
trx: Transaction
|
||||||
) {
|
) {
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const [user] = await trx
|
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(eq(users.username, username), eq(users.email, username)),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -556,7 +537,7 @@ async function addUserPolicies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.user.userId,
|
userId: user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1378,6 +1378,12 @@ if (build !== "saas") {
|
|||||||
user.adminGeneratePasswordResetCode
|
user.adminGeneratePasswordResetCode
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/user/:userId/promote-server-admin",
|
||||||
|
verifyUserIsServerAdmin,
|
||||||
|
user.adminPromoteServerAdmin
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/user/:userId",
|
"/user/:userId",
|
||||||
verifyUserIsServerAdmin,
|
verifyUserIsServerAdmin,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { db, idp, users } from "@server/db";
|
|||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { and, asc, desc, eq, like, or, sql } from "drizzle-orm";
|
import { and, asc, desc, eq, like, or, sql, type SQL } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
@@ -196,7 +196,7 @@ export async function adminListUsers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const conditions = [eq(users.serverAdmin, false)];
|
const conditions: Array<SQL<unknown> | undefined> = [];
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
const q = "%" + query.toLowerCase() + "%";
|
const q = "%" + query.toLowerCase() + "%";
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, users } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
|
||||||
|
|
||||||
|
const promoteServerAdminParamsSchema = z.strictObject({
|
||||||
|
userId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AdminPromoteServerAdminResponse = {
|
||||||
|
userId: string;
|
||||||
|
serverAdmin: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AdminPromoteServerAdminResponseDataSchema = z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
serverAdmin: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/user/{userId}/promote-server-admin",
|
||||||
|
description: "Promote a user to server admin (server admin).",
|
||||||
|
tags: [OpenAPITags.User],
|
||||||
|
request: {
|
||||||
|
params: promoteServerAdminParamsSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successful response",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: createApiResponseSchema(
|
||||||
|
AdminPromoteServerAdminResponseDataSchema
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function adminPromoteServerAdmin(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = promoteServerAdminParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [existingUser] = await db
|
||||||
|
.select({
|
||||||
|
userId: users.userId,
|
||||||
|
serverAdmin: users.serverAdmin
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
return next(createHttpError(HttpCode.NOT_FOUND, "User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUser.serverAdmin) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"User is already a server admin"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Promoting user ${userId} to server admin (by ${req.user?.userId})`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({ serverAdmin: true })
|
||||||
|
.where(eq(users.userId, userId));
|
||||||
|
|
||||||
|
return response<AdminPromoteServerAdminResponse>(res, {
|
||||||
|
data: {
|
||||||
|
userId: existingUser.userId,
|
||||||
|
serverAdmin: true
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "User promoted to server admin successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export * from "./adminListUsers";
|
|||||||
export * from "./adminRemoveUser";
|
export * from "./adminRemoveUser";
|
||||||
export * from "./adminGetUser";
|
export * from "./adminGetUser";
|
||||||
export * from "./adminGeneratePasswordResetCode";
|
export * from "./adminGeneratePasswordResetCode";
|
||||||
|
export * from "./adminPromoteServerAdmin";
|
||||||
export * from "./listInvitations";
|
export * from "./listInvitations";
|
||||||
export * from "./removeInvitation";
|
export * from "./removeInvitation";
|
||||||
export * from "./createOrgUser";
|
export * from "./createOrgUser";
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { AxiosResponse } from "axios";
|
|||||||
import { ListRolesResponse } from "@server/routers/role";
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
|
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
|
||||||
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
|
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
|
||||||
|
import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import {
|
import {
|
||||||
@@ -75,6 +76,12 @@ export default function GeneralPage() {
|
|||||||
>([createMappingBuilderRule()]);
|
>([createMappingBuilderRule()]);
|
||||||
const [rawRoleExpression, setRawRoleExpression] = useState("");
|
const [rawRoleExpression, setRawRoleExpression] = useState("");
|
||||||
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
||||||
|
const [originalIdentifierPath, setOriginalIdentifierPath] = useState("");
|
||||||
|
const [identifierConfirmOpen, setIdentifierConfirmOpen] = useState(false);
|
||||||
|
const [pendingPayload, setPendingPayload] = useState<Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
||||||
const [redirectUrl, setRedirectUrl] = useState(
|
const [redirectUrl, setRedirectUrl] = useState(
|
||||||
@@ -184,6 +191,9 @@ export default function GeneralPage() {
|
|||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
const roleMapping = data.idpOrg.roleMapping;
|
const roleMapping = data.idpOrg.roleMapping;
|
||||||
const idpVariant = data.idpOidcConfig?.variant || "oidc";
|
const idpVariant = data.idpOidcConfig?.variant || "oidc";
|
||||||
|
setOriginalIdentifierPath(
|
||||||
|
data.idpOidcConfig?.identifierPath ?? "sub"
|
||||||
|
);
|
||||||
setRedirectUrl(res.data.data.redirectUrl);
|
setRedirectUrl(res.data.data.redirectUrl);
|
||||||
|
|
||||||
// Set the variant
|
// Set the variant
|
||||||
@@ -378,18 +388,56 @@ export default function GeneralPage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api.post(
|
const nextIdentifierPath =
|
||||||
`/org/${orgId}/idp/${idpId}/oidc`,
|
variant === "oidc"
|
||||||
payload
|
? (data as OidcFormValues).identifierPath
|
||||||
);
|
: undefined;
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (
|
||||||
toast({
|
typeof nextIdentifierPath === "string" &&
|
||||||
title: t("success"),
|
nextIdentifierPath !== originalIdentifierPath
|
||||||
description: t("idpUpdatedDescription")
|
) {
|
||||||
});
|
setPendingPayload(payload);
|
||||||
router.refresh();
|
setIdentifierConfirmOpen(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await persistIdp(payload);
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistIdp(payload: Record<string, unknown>) {
|
||||||
|
const res = await api.post(`/org/${orgId}/idp/${idpId}/oidc`, payload);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
if (typeof payload.identifierPath === "string") {
|
||||||
|
setOriginalIdentifierPath(payload.identifierPath);
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("idpUpdatedDescription")
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmIdentifierChange() {
|
||||||
|
if (!pendingPayload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await persistIdp(pendingPayload);
|
||||||
|
setPendingPayload(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
@@ -407,6 +455,16 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<IdpIdentifierChangeDialog
|
||||||
|
open={identifierConfirmOpen}
|
||||||
|
setOpen={(open) => {
|
||||||
|
setIdentifierConfirmOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setPendingPayload(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onConfirm={confirmIdentifierChange}
|
||||||
|
/>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
|
import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
@@ -51,6 +52,12 @@ export default function GeneralPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
||||||
|
const [originalIdentifierPath, setOriginalIdentifierPath] = useState("");
|
||||||
|
const [identifierConfirmOpen, setIdentifierConfirmOpen] = useState(false);
|
||||||
|
const [pendingPayload, setPendingPayload] = useState<Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -141,6 +148,9 @@ export default function GeneralPage() {
|
|||||||
| "google"
|
| "google"
|
||||||
| "azure") || "oidc";
|
| "azure") || "oidc";
|
||||||
setVariant(idpVariant);
|
setVariant(idpVariant);
|
||||||
|
setOriginalIdentifierPath(
|
||||||
|
data.idpOidcConfig?.identifierPath ?? "sub"
|
||||||
|
);
|
||||||
|
|
||||||
let tenantId = "";
|
let tenantId = "";
|
||||||
if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) {
|
if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) {
|
||||||
@@ -258,15 +268,56 @@ export default function GeneralPage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api.post(`/idp/${idpId}/oidc`, payload);
|
const nextIdentifierPath =
|
||||||
|
variant === "oidc"
|
||||||
|
? (data as OidcFormValues).identifierPath
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (
|
||||||
toast({
|
typeof nextIdentifierPath === "string" &&
|
||||||
title: t("success"),
|
nextIdentifierPath !== originalIdentifierPath
|
||||||
description: t("idpUpdatedDescription")
|
) {
|
||||||
});
|
setPendingPayload(payload);
|
||||||
router.refresh();
|
setIdentifierConfirmOpen(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await persistIdp(payload);
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistIdp(payload: Record<string, unknown>) {
|
||||||
|
const res = await api.post(`/idp/${idpId}/oidc`, payload);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
if (typeof payload.identifierPath === "string") {
|
||||||
|
setOriginalIdentifierPath(payload.identifierPath);
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("idpUpdatedDescription")
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmIdentifierChange() {
|
||||||
|
if (!pendingPayload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await persistIdp(pendingPayload);
|
||||||
|
setPendingPayload(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
@@ -284,6 +335,16 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<IdpIdentifierChangeDialog
|
||||||
|
open={identifierConfirmOpen}
|
||||||
|
setOpen={(open) => {
|
||||||
|
setIdentifierConfirmOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setPendingPayload(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onConfirm={confirmIdentifierChange}
|
||||||
|
/>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ export default async function UsersPage(props: AdminUsersPageProps) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
userRows
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowUp10Icon,
|
ArrowUp10Icon,
|
||||||
ChevronsUpDownIcon,
|
ChevronsUpDownIcon,
|
||||||
MoreHorizontal
|
MoreHorizontal,
|
||||||
|
ShieldUserIcon
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -43,6 +44,14 @@ import {
|
|||||||
CredenzaClose
|
CredenzaClose
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "./ui/tooltip";
|
||||||
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
|
||||||
export type GlobalUserRow = {
|
export type GlobalUserRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -90,6 +99,9 @@ export default function UsersTable({
|
|||||||
const [passwordResetCodeData, setPasswordResetCodeData] =
|
const [passwordResetCodeData, setPasswordResetCodeData] =
|
||||||
useState<AdminGeneratePasswordResetCodeResponse | null>(null);
|
useState<AdminGeneratePasswordResetCodeResponse | null>(null);
|
||||||
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
const [isGeneratingCode, setIsGeneratingCode] = useState(false);
|
||||||
|
const [isPromoteModalOpen, setIsPromoteModalOpen] = useState(false);
|
||||||
|
const [promoting, setPromoting] = useState<GlobalUserRow | null>(null);
|
||||||
|
const user = useUserContext();
|
||||||
|
|
||||||
const [isRefreshing, startTransition] = useTransition();
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
const {
|
const {
|
||||||
@@ -184,6 +196,37 @@ export default function UsersTable({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const promoteToServerAdmin = async (user: GlobalUserRow) => {
|
||||||
|
try {
|
||||||
|
await api.post(`/user/${user.id}/promote-server-admin`);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("promoteServerAdminSuccess"),
|
||||||
|
description: t("promoteServerAdminSuccessDescription", {
|
||||||
|
selectedUser: getUserDisplayName({
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
username: user.username
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(t("promoteServerAdminError"), e);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("promoteServerAdminError"),
|
||||||
|
description: formatAxiosError(e, t("promoteServerAdminError"))
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsPromoteModalOpen(false);
|
||||||
|
setPromoting(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function toggleSort(column: string) {
|
function toggleSort(column: string) {
|
||||||
const newSearch = getNextSortOrder(column, searchParams);
|
const newSearch = getNextSortOrder(column, searchParams);
|
||||||
filter({
|
filter({
|
||||||
@@ -235,7 +278,35 @@ export default function UsersTable({
|
|||||||
<Icon className="ml-2 h-4 w-4" />
|
<Icon className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="inline-flex gap-1 items-center">
|
||||||
|
{row.original.username}{" "}
|
||||||
|
{row.original.id === user.user.userId && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
·
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-primary">you</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{row.original.serverAdmin && (
|
||||||
|
<>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<ShieldUserIcon className="text-primary size-5 flex-none" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t("serverAdmin")}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
{/* <Badge>{t("serverAdmin")}</Badge> */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "email",
|
accessorKey: "email",
|
||||||
@@ -369,11 +440,22 @@ export default function UsersTable({
|
|||||||
{t("generatePasswordResetCode")}
|
{t("generatePasswordResetCode")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{!r.serverAdmin && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setPromoting(r);
|
||||||
|
setIsPromoteModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("promoteServerAdmin")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelected(r);
|
setSelected(r);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}}
|
}}
|
||||||
|
className="text-red-400"
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -435,6 +517,42 @@ export default function UsersTable({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{promoting && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={isPromoteModalOpen}
|
||||||
|
setOpen={(val) => {
|
||||||
|
setIsPromoteModalOpen(val);
|
||||||
|
if (!val) {
|
||||||
|
setPromoting(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>
|
||||||
|
{t("promoteServerAdminQuestion", {
|
||||||
|
selectedUser: getUserDisplayName({
|
||||||
|
email: promoting.email,
|
||||||
|
name: promoting.name,
|
||||||
|
username: promoting.username
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>{t("promoteServerAdminMessage")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("promoteServerAdminConfirm")}
|
||||||
|
onConfirm={async () => promoteToServerAdmin(promoting)}
|
||||||
|
string={getUserDisplayName({
|
||||||
|
email: promoting.email,
|
||||||
|
name: promoting.name,
|
||||||
|
username: promoting.username
|
||||||
|
})}
|
||||||
|
warningText={t("promoteServerAdminWarning")}
|
||||||
|
title={t("promoteServerAdminTitle")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ControlledDataTable
|
<ControlledDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rows={users}
|
rows={users}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
type IdpIdentifierChangeDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
onConfirm: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function IdpIdentifierChangeDialog({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
onConfirm
|
||||||
|
}: IdpIdentifierChangeDialogProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("idpIdentifierChangeDescription")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("saveGeneralSettings")}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
string={t("idpIdentifierChangeConfirmMessage")}
|
||||||
|
title={t("idpIdentifierChangeTitle")}
|
||||||
|
warningText={t("idpIdentifierChangeWarningText")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -155,13 +155,13 @@ export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CommandList className="max-h-118 min-h-0 h-(--cmdk-list-height) scroll-pb-4 scroll-pt-2 transition-[height] duration-250 ease-in-out">
|
<CommandList className="max-h-118 min-h-0 h-(--cmdk-list-height) scroll-pb-4 scroll-pt-2 transition-[height] duration-250 ease-in-out">
|
||||||
<CommandEmpty>{t("commandPaletteNoResults")}</CommandEmpty>
|
|
||||||
|
|
||||||
<CommandGroup
|
<CommandGroup
|
||||||
heading={t("commandActionModeInfo")}
|
heading={t("commandActionModeInfo")}
|
||||||
className="[&_[cmdk-group-heading]]:text-sm"
|
className="[&_[cmdk-group-heading]]:text-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CommandEmpty>{t("commandPaletteNoResults")}</CommandEmpty>
|
||||||
|
|
||||||
{!isActionMode &&
|
{!isActionMode &&
|
||||||
navigationGroups.map((group, groupIndex) => (
|
navigationGroups.map((group, groupIndex) => (
|
||||||
<React.Fragment key={group.heading}>
|
<React.Fragment key={group.heading}>
|
||||||
|
|||||||
Reference in New Issue
Block a user