mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-12 03:09:54 +00:00
Compare commits
5 Commits
1.18.3-s.3
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77d17af15b | ||
|
|
264c6bf4e8 | ||
|
|
4aa72eb1a3 | ||
|
|
a066a68e1a | ||
|
|
9fb677e952 |
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "An error occurred deleting link",
|
"shareErrorDeleteMessage": "An error occurred deleting link",
|
||||||
"shareDeleted": "Link deleted",
|
"shareDeleted": "Link deleted",
|
||||||
"shareDeletedDescription": "The link has been deleted",
|
"shareDeletedDescription": "The link has been deleted",
|
||||||
|
"shareDelete": "Delete Share Link",
|
||||||
|
"shareDeleteConfirm": "Confirm Delete Share Link",
|
||||||
|
"shareQuestionRemove": "Are you sure you want to delete this share link?",
|
||||||
|
"shareMessageRemove": "Once deleted, the link will no longer work and anyone using it will lose access to the resource.",
|
||||||
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
||||||
"accessToken": "Access Token",
|
"accessToken": "Access Token",
|
||||||
"usageExamples": "Usage Examples",
|
"usageExamples": "Usage Examples",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
||||||
"userRemoveOrgConfirm": "Confirm Remove User",
|
"userRemoveOrgConfirm": "Confirm Remove User",
|
||||||
"userRemoveOrg": "Remove User from Organization",
|
"userRemoveOrg": "Remove User from Organization",
|
||||||
|
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||||
|
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||||
|
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||||
|
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"accessRoleMember": "Member",
|
"accessRoleMember": "Member",
|
||||||
"accessRoleOwner": "Owner",
|
"accessRoleOwner": "Owner",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Invalid email address",
|
"emailInvalid": "Invalid email address",
|
||||||
"inviteValidityDuration": "Please select a duration",
|
"inviteValidityDuration": "Please select a duration",
|
||||||
"accessRoleSelectPlease": "Please select a role",
|
"accessRoleSelectPlease": "Please select a role",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||||
|
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||||
"usernameRequired": "Username is required",
|
"usernameRequired": "Username is required",
|
||||||
"idpSelectPlease": "Please select an identity provider",
|
"idpSelectPlease": "Please select an identity provider",
|
||||||
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
||||||
|
|||||||
@@ -1227,7 +1227,11 @@ async function getDomainId(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const domainSelection = validDomains[0].domains;
|
// Pick the most specific (longest baseDomain) valid domain so that, e.g.,
|
||||||
|
// *.test.dev.example.com is assigned to *.dev.example.com rather than *.example.com.
|
||||||
|
const domainSelection = validDomains.sort(
|
||||||
|
(a, b) => b.domains.baseDomain.length - a.domains.baseDomain.length
|
||||||
|
)[0].domains;
|
||||||
const baseDomain = domainSelection.baseDomain;
|
const baseDomain = domainSelection.baseDomain;
|
||||||
|
|
||||||
// Wildcard full-domains are not allowed on namespace (provided/free) domains
|
// Wildcard full-domains are not allowed on namespace (provided/free) domains
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ export class PrivateConfig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.env.BRANDING_HIDE_POWERED_BY =
|
||||||
|
this.rawPrivateConfig.branding?.hide_powered_by === true ||
|
||||||
|
this.rawPrivateConfig.branding?.resource_auth_page
|
||||||
|
?.hide_powered_by === true
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
|
||||||
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
||||||
this.rawPrivateConfig.branding?.login_page?.subtitle_text || "";
|
this.rawPrivateConfig.branding?.login_page?.subtitle_text || "";
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const privateConfigSchema = z
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
hide_auth_layout_footer: z.boolean().optional().default(false),
|
hide_auth_layout_footer: z.boolean().optional().default(false),
|
||||||
|
hide_powered_by: z.boolean().optional(),
|
||||||
login_page: z
|
login_page: z
|
||||||
.object({
|
.object({
|
||||||
subtitle_text: z.string().optional()
|
subtitle_text: z.string().optional()
|
||||||
|
|||||||
@@ -98,15 +98,6 @@ export async function addUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser[0].isOwner) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Cannot change the role of the owner of the organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleExists = await db
|
const roleExists = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
|
|||||||
@@ -98,11 +98,11 @@ export async function removeUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
if (existingUser.isOwner && role.isAdmin === true) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"Cannot change the roles of the owner of the organization"
|
"Cannot remove the administrator role from the organization owner"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,17 +87,8 @@ export async function setUserOrgRoles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Cannot change the roles of the owner of the organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgRoles = await db
|
const orgRoles = await db
|
||||||
.select({ roleId: roles.roleId })
|
.select({ roleId: roles.roleId, isAdmin: roles.isAdmin })
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -115,6 +106,18 @@ export async function setUserOrgRoles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingUser.isOwner) {
|
||||||
|
const hasAdminRole = orgRoles.some((r) => r.isAdmin === true);
|
||||||
|
if (!hasAdminRole) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"The organization owner must retain an administrator role"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let orgClientsToRebuild: Client[] = [];
|
let orgClientsToRebuild: Client[] = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ export async function addUserRoleLegacy(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
if (existingUser.isOwner && role.isAdmin !== true) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"Cannot change the role of the owner of the organization"
|
"The organization owner must retain an administrator role"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ export async function queryUser(orgId: string, userId: string) {
|
|||||||
.from(userOrgRoles)
|
.from(userOrgRoles)
|
||||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
|
||||||
eq(userOrgRoles.userId, userId),
|
|
||||||
eq(userOrgRoles.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAdmin = roleRows.some((r) => r.isAdmin);
|
const isAdmin = roleRows.some((r) => r.isAdmin);
|
||||||
@@ -61,7 +58,8 @@ export async function queryUser(orgId: string, userId: string) {
|
|||||||
roleIds: roleRows.map((r) => r.roleId),
|
roleIds: roleRows.map((r) => r.roleId),
|
||||||
roles: roleRows.map((r) => ({
|
roles: roleRows.map((r) => ({
|
||||||
roleId: r.roleId,
|
roleId: r.roleId,
|
||||||
name: r.roleName ?? ""
|
name: r.roleName ?? "",
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
import OrgRolesTagField from "@app/components/OrgRolesTagField";
|
import OrgRolesTagField from "@app/components/OrgRolesTagField";
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +26,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
|
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
@@ -32,7 +34,7 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
|||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useActionState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -42,13 +44,15 @@ const accessControlsFormSchema = z.object({
|
|||||||
roles: z.array(
|
roles: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
text: z.string()
|
text: z.string(),
|
||||||
|
isAdmin: z.boolean().optional()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function AccessControlsPage() {
|
export default function AccessControlsPage() {
|
||||||
const { orgUser: user, updateOrgUser } = userOrgUserContext();
|
const { orgUser: user, updateOrgUser } = userOrgUserContext();
|
||||||
|
const { user: sessionUser } = useUserContext();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
@@ -72,7 +76,8 @@ export default function AccessControlsPage() {
|
|||||||
autoProvisioned: user.autoProvisioned || false,
|
autoProvisioned: user.autoProvisioned || false,
|
||||||
roles: (user.roles ?? []).map((r) => ({
|
roles: (user.roles ?? []).map((r) => ({
|
||||||
id: r.roleId.toString(),
|
id: r.roleId.toString(),
|
||||||
text: r.name
|
text: r.name,
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -84,7 +89,8 @@ export default function AccessControlsPage() {
|
|||||||
"roles",
|
"roles",
|
||||||
(user.roles ?? []).map((r) => ({
|
(user.roles ?? []).map((r) => ({
|
||||||
id: r.roleId.toString(),
|
id: r.roleId.toString(),
|
||||||
text: r.name
|
text: r.name,
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
form.setValue("autoProvisioned", user.autoProvisioned || false);
|
||||||
@@ -95,11 +101,11 @@ export default function AccessControlsPage() {
|
|||||||
? t("singleRolePerUserPlanNotice")
|
? t("singleRolePerUserPlanNotice")
|
||||||
: t("singleRolePerUserEditionNotice");
|
: t("singleRolePerUserEditionNotice");
|
||||||
|
|
||||||
const [, action, isSubmitting] = useActionState(onSubmit, null);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
async function onSubmit() {
|
const [confirmRemoveOwnAdminOpen, setConfirmRemoveOwnAdminOpen] =
|
||||||
const isValid = await form.trigger();
|
useState(false);
|
||||||
if (!isValid) return;
|
|
||||||
|
|
||||||
|
async function executeSave() {
|
||||||
const values = form.getValues();
|
const values = form.getValues();
|
||||||
|
|
||||||
if (values.roles.length === 0) {
|
if (values.roles.length === 0) {
|
||||||
@@ -111,6 +117,7 @@ export default function AccessControlsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
const updateRoleRequest = supportsMultipleRolesPerUser
|
const updateRoleRequest = supportsMultipleRolesPerUser
|
||||||
@@ -130,7 +137,8 @@ export default function AccessControlsPage() {
|
|||||||
roleIds,
|
roleIds,
|
||||||
roles: values.roles.map((r) => ({
|
roles: values.roles.map((r) => ({
|
||||||
roleId: parseInt(r.id, 10),
|
roleId: parseInt(r.id, 10),
|
||||||
name: r.text
|
name: r.text,
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
})),
|
})),
|
||||||
autoProvisioned: values.autoProvisioned
|
autoProvisioned: values.autoProvisioned
|
||||||
});
|
});
|
||||||
@@ -149,11 +157,61 @@ export default function AccessControlsPage() {
|
|||||||
t("accessRoleErrorAddDescription")
|
t("accessRoleErrorAddDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAccessControlsSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const isValid = await form.trigger();
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
const values = form.getValues();
|
||||||
|
|
||||||
|
if (values.roles.length === 0) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("accessRoleErrorAdd"),
|
||||||
|
description: t("accessRoleSelectPlease")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const willHaveAdminRole = values.roles.some(
|
||||||
|
(r) => r.isAdmin === true
|
||||||
|
);
|
||||||
|
|
||||||
|
const isRemovingOwnAdmin =
|
||||||
|
sessionUser.userId === user.userId &&
|
||||||
|
user.isAdmin &&
|
||||||
|
!willHaveAdminRole;
|
||||||
|
|
||||||
|
if (isRemovingOwnAdmin) {
|
||||||
|
setConfirmRemoveOwnAdminOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await executeSave();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={confirmRemoveOwnAdminOpen}
|
||||||
|
setOpen={setConfirmRemoveOwnAdminOpen}
|
||||||
|
title={t("removeOwnAdminRoleConfirmTitle")}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("removeOwnAdminRoleConfirmDescription")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("removeOwnAdminRoleConfirmButton")}
|
||||||
|
string={t("removeOwnAdminRoleConfirmPhrase")}
|
||||||
|
onConfirm={executeSave}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
@@ -168,7 +226,7 @@ export default function AccessControlsPage() {
|
|||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
action={action}
|
onSubmit={(e) => void handleAccessControlsSubmit(e)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="access-controls-form"
|
id="access-controls-form"
|
||||||
>
|
>
|
||||||
@@ -237,8 +295,8 @@ export default function AccessControlsPage() {
|
|||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isSubmitting}
|
loading={isSaving}
|
||||||
disabled={isSubmitting}
|
disabled={isSaving}
|
||||||
form="access-controls-form"
|
form="access-controls-form"
|
||||||
>
|
>
|
||||||
{t("accessControlsSubmit")}
|
{t("accessControlsSubmit")}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function InviteStatusCard({
|
|||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
} else if (!user && type === "not_logged_in") {
|
} else if (!user && type === "not_logged_in") {
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
|
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
|
||||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
} else {
|
} else {
|
||||||
@@ -113,7 +113,7 @@ export default function InviteStatusCard({
|
|||||||
async function goToLogin() {
|
async function goToLogin() {
|
||||||
await api.post("/auth/logout", {});
|
await api.post("/auth/logout", {});
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
|
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
|
||||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Link from "next/link";
|
|||||||
import { replacePlaceholder } from "@app/lib/replacePlaceholder";
|
import { replacePlaceholder } from "@app/lib/replacePlaceholder";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
type OrgLoginPageProps = {
|
type OrgLoginPageProps = {
|
||||||
loginPage: LoadLoginPageResponse | undefined;
|
loginPage: LoadLoginPageResponse | undefined;
|
||||||
@@ -52,19 +53,21 @@ export default async function OrgLoginPage({
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-center mb-2">
|
{build !== "enterprise" || !env.branding.hidePoweredBy ? (
|
||||||
<span className="text-sm text-muted-foreground">
|
<div className="text-center mb-2">
|
||||||
{t("poweredBy")}{" "}
|
<span className="text-sm text-muted-foreground">
|
||||||
<Link
|
{t("poweredBy")}{" "}
|
||||||
href="https://pangolin.net/"
|
<Link
|
||||||
target="_blank"
|
href="https://pangolin.net/"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="underline"
|
rel="noopener noreferrer"
|
||||||
>
|
className="underline"
|
||||||
{env.branding.appName || "Pangolin"}
|
>
|
||||||
</Link>
|
{env.branding.appName || "Pangolin"}
|
||||||
</span>
|
</Link>
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
{branding?.logoUrl && (
|
{branding?.logoUrl && (
|
||||||
|
|||||||
@@ -375,7 +375,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
{!accessDenied ? (
|
{!accessDenied ? (
|
||||||
<div>
|
<div>
|
||||||
{isUnlocked() && build === "enterprise" ? (
|
{isUnlocked() && build === "enterprise" ? (
|
||||||
!env.branding.resourceAuthPage?.hidePoweredBy && (
|
!env.branding.resourceAuthPage?.hidePoweredBy &&
|
||||||
|
!env.branding.hidePoweredBy && (
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{t("poweredBy")}{" "}
|
{t("poweredBy")}{" "}
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ export default function ShareLinksTable({
|
|||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [selectedLink, setSelectedLink] = useState<ShareLinkRow | null>(null);
|
||||||
const [rows, setRows] = useState<ShareLinkRow[]>(shareLinks);
|
const [rows, setRows] = useState<ShareLinkRow[]>(shareLinks);
|
||||||
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
@@ -92,6 +94,7 @@ export default function ShareLinksTable({
|
|||||||
title: t("shareErrorDelete"),
|
title: t("shareErrorDelete"),
|
||||||
description: formatAxiosError(e, t("shareErrorDeleteMessage"))
|
description: formatAxiosError(e, t("shareErrorDeleteMessage"))
|
||||||
});
|
});
|
||||||
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
const newRows = rows.filter((r) => r.accessTokenId !== id);
|
const newRows = rows.filter((r) => r.accessTokenId !== id);
|
||||||
@@ -293,9 +296,10 @@ export default function ShareLinksTable({
|
|||||||
{/* </DropdownMenu> */}
|
{/* </DropdownMenu> */}
|
||||||
<Button
|
<Button
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
deleteSharelink(row.original.accessTokenId)
|
setSelectedLink(resourceRow);
|
||||||
}
|
setIsDeleteModalOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -307,6 +311,30 @@ export default function ShareLinksTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{selectedLink && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={isDeleteModalOpen}
|
||||||
|
setOpen={(val) => {
|
||||||
|
setIsDeleteModalOpen(val);
|
||||||
|
if (!val) setSelectedLink(null);
|
||||||
|
}}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("shareQuestionRemove")}</p>
|
||||||
|
<p>{t("shareMessageRemove")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("shareDeleteConfirm")}
|
||||||
|
onConfirm={async () =>
|
||||||
|
deleteSharelink(selectedLink.accessTokenId)
|
||||||
|
}
|
||||||
|
string={
|
||||||
|
selectedLink.title || selectedLink.resourceName
|
||||||
|
}
|
||||||
|
title={t("shareDelete")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<CreateShareLinkForm
|
<CreateShareLinkForm
|
||||||
open={isCreateModalOpen}
|
open={isCreateModalOpen}
|
||||||
setOpen={setIsCreateModalOpen}
|
setOpen={setIsCreateModalOpen}
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ export default function UsersTable({
|
|||||||
];
|
];
|
||||||
}, [searchParams.toString()]);
|
}, [searchParams.toString()]);
|
||||||
|
|
||||||
|
const isRemovingSelf = useMemo(() => {
|
||||||
|
if (!selectedUser || !user) return false;
|
||||||
|
return (
|
||||||
|
`${selectedUser.username}-${selectedUser.idpId}` ===
|
||||||
|
`${user.username}-${user.idpId}`
|
||||||
|
);
|
||||||
|
}, [selectedUser, user]);
|
||||||
|
|
||||||
function handleFilterChange(
|
function handleFilterChange(
|
||||||
column: string,
|
column: string,
|
||||||
value: string | undefined | null
|
value: string | undefined | null
|
||||||
@@ -223,10 +231,7 @@ export default function UsersTable({
|
|||||||
header: () => <span className="p-3"></span>,
|
header: () => <span className="p-3"></span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const userRow = row.original;
|
const userRow = row.original;
|
||||||
const isCurrentUser =
|
const canRemoveFromOrg = !userRow.isOwner;
|
||||||
`${userRow.username}-${userRow.idpId}` ===
|
|
||||||
`${user?.username}-${user?.idpId}`;
|
|
||||||
const isDisabled = userRow.isOwner || isCurrentUser;
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div>
|
<div>
|
||||||
@@ -235,7 +240,6 @@ export default function UsersTable({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
>
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
{t("openMenu")}
|
{t("openMenu")}
|
||||||
@@ -247,16 +251,12 @@ export default function UsersTable({
|
|||||||
<Link
|
<Link
|
||||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||||
className="block w-full"
|
className="block w-full"
|
||||||
aria-disabled={isDisabled}
|
|
||||||
onClick={(e) =>
|
|
||||||
isDisabled && e.preventDefault()
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DropdownMenuItem disabled={isDisabled}>
|
<DropdownMenuItem>
|
||||||
{t("accessUserManage")}
|
{t("accessUserManage")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
{!isDisabled && (
|
{canRemoveFromOrg && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@@ -271,25 +271,14 @@ export default function UsersTable({
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
{isDisabled ? (
|
<Link
|
||||||
<Button
|
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||||
variant={"outline"}
|
>
|
||||||
className="ml-2"
|
<Button variant={"outline"} className="ml-2">
|
||||||
disabled
|
|
||||||
>
|
|
||||||
{t("manage")}
|
{t("manage")}
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
</Link>
|
||||||
<Link
|
|
||||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
|
||||||
>
|
|
||||||
<Button variant={"outline"} className="ml-2">
|
|
||||||
{t("manage")}
|
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -359,22 +348,45 @@ export default function UsersTable({
|
|||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p>{t("userQuestionOrgRemove")}</p>
|
<p>
|
||||||
<p>{t("userMessageOrgRemove")}</p>
|
{t(
|
||||||
|
isRemovingSelf
|
||||||
|
? "userQuestionOrgRemoveSelf"
|
||||||
|
: "userQuestionOrgRemove"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
isRemovingSelf
|
||||||
|
? "userMessageOrgRemoveSelf"
|
||||||
|
: "userMessageOrgRemove"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t("userRemoveOrgConfirm")}
|
buttonText={t(
|
||||||
|
isRemovingSelf
|
||||||
|
? "userRemoveOrgConfirmSelf"
|
||||||
|
: "userRemoveOrgConfirm"
|
||||||
|
)}
|
||||||
|
warningText={
|
||||||
|
isRemovingSelf ? t("userRemoveOrgSelfWarning") : undefined
|
||||||
|
}
|
||||||
onConfirm={async () => startTransition(removeUser)}
|
onConfirm={async () => startTransition(removeUser)}
|
||||||
string={
|
string={
|
||||||
selectedUser
|
isRemovingSelf
|
||||||
? getUserDisplayName({
|
? t("userRemoveOrgConfirmPhraseSelf")
|
||||||
email: selectedUser.email,
|
: selectedUser
|
||||||
name: selectedUser.name,
|
? getUserDisplayName({
|
||||||
username: selectedUser.username
|
email: selectedUser.email,
|
||||||
})
|
name: selectedUser.name,
|
||||||
: ""
|
username: selectedUser.username
|
||||||
|
})
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
title={t("userRemoveOrg")}
|
title={t(
|
||||||
|
isRemovingSelf ? "userRemoveOrgSelf" : "userRemoveOrg"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ControlledDataTable
|
<ControlledDataTable
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { cn } from "@app/lib/cn";
|
|||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export type TagValue = { text: string; id: string };
|
export type TagValue = { text: string; id: string; isAdmin?: boolean };
|
||||||
|
|
||||||
export type MultiSelectTagsProps<T extends TagValue> = {
|
export type MultiSelectTagsProps<T extends TagValue> = {
|
||||||
emptyPlaceholder?: string;
|
emptyPlaceholder?: string;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useDebounce } from "use-debounce";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
|
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
|
||||||
|
|
||||||
export type SelectedRole = { id: string; text: string };
|
export type SelectedRole = { id: string; text: string; isAdmin?: boolean };
|
||||||
|
|
||||||
export type RolesSelectorProps = {
|
export type RolesSelectorProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ export function pullEnv(): Env {
|
|||||||
process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER === "true"
|
process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER === "true"
|
||||||
? true
|
? true
|
||||||
: false,
|
: false,
|
||||||
|
hidePoweredBy:
|
||||||
|
process.env.BRANDING_HIDE_POWERED_BY === "true" ? true : false,
|
||||||
logo: {
|
logo: {
|
||||||
lightPath: process.env.BRANDING_LOGO_LIGHT_PATH as string,
|
lightPath: process.env.BRANDING_LOGO_LIGHT_PATH as string,
|
||||||
darkPath: process.env.BRANDING_LOGO_DARK_PATH as string,
|
darkPath: process.env.BRANDING_LOGO_DARK_PATH as string,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type Env = {
|
|||||||
appName?: string;
|
appName?: string;
|
||||||
background_image_path?: string;
|
background_image_path?: string;
|
||||||
hideAuthLayoutFooter?: boolean;
|
hideAuthLayoutFooter?: boolean;
|
||||||
|
hidePoweredBy?: boolean;
|
||||||
logo?: {
|
logo?: {
|
||||||
lightPath?: string;
|
lightPath?: string;
|
||||||
darkPath?: string;
|
darkPath?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user