forked from baron/baron-sso
ci: add code check badges and coverage reports
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { AlertTriangle, ExternalLink, LogIn, ShieldHalf } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Upload,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { PageHeader } from "../../../../common/core/components/page";
|
||||
@@ -32,6 +32,13 @@ import { Label } from "../../components/ui/label";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { toast } from "../../components/ui/use-toast";
|
||||
import type {
|
||||
ClientStatus,
|
||||
ClientType,
|
||||
ClientUpsertRequest,
|
||||
MyTenantSummary,
|
||||
TenantSummary,
|
||||
} from "../../lib/devApi";
|
||||
import {
|
||||
type ClientRelation,
|
||||
createClient,
|
||||
@@ -44,13 +51,6 @@ import {
|
||||
updateClient,
|
||||
updateClientStatus,
|
||||
} from "../../lib/devApi";
|
||||
import type {
|
||||
ClientStatus,
|
||||
ClientType,
|
||||
ClientUpsertRequest,
|
||||
MyTenantSummary,
|
||||
TenantSummary,
|
||||
} from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { resolveProfileRole } from "../../lib/role";
|
||||
import { cn } from "../../lib/utils";
|
||||
@@ -408,6 +408,59 @@ function ClientGeneralPage() {
|
||||
]);
|
||||
const [idTokenClaims, setIdTokenClaims] = useState<IdTokenClaimItem[]>([]);
|
||||
|
||||
const tenantScopeDescription = t(
|
||||
"msg.dev.clients.scopes.tenant",
|
||||
"소속 테넌트 정보 접근",
|
||||
);
|
||||
|
||||
const buildTenantScope = useCallback(
|
||||
(id: string): ScopeItem => ({
|
||||
id,
|
||||
name: "tenant",
|
||||
description: tenantScopeDescription,
|
||||
mandatory: true,
|
||||
locked: true,
|
||||
}),
|
||||
[tenantScopeDescription],
|
||||
);
|
||||
|
||||
const normalizeScopesForTenantAccess = useCallback(
|
||||
(nextScopes: ScopeItem[], restricted: boolean): ScopeItem[] => {
|
||||
const normalized = nextScopes.map((scope) => {
|
||||
if (scope.name.trim() !== "tenant") {
|
||||
return scope;
|
||||
}
|
||||
return {
|
||||
...scope,
|
||||
description: scope.description || tenantScopeDescription,
|
||||
mandatory: restricted,
|
||||
locked: restricted,
|
||||
};
|
||||
});
|
||||
|
||||
if (
|
||||
restricted &&
|
||||
!normalized.some((scope) => scope.name.trim() === "tenant")
|
||||
) {
|
||||
normalized.push(buildTenantScope(`tenant-${Date.now()}`));
|
||||
}
|
||||
|
||||
const openidScopes = normalized.filter(
|
||||
(scope) => scope.name.trim() === "openid",
|
||||
);
|
||||
const tenantScopes = normalized.filter(
|
||||
(scope) => scope.name.trim() === "tenant",
|
||||
);
|
||||
const remainingScopes = normalized.filter((scope) => {
|
||||
const name = scope.name.trim();
|
||||
return name !== "openid" && name !== "tenant";
|
||||
});
|
||||
|
||||
return [...openidScopes, ...tenantScopes, ...remainingScopes];
|
||||
},
|
||||
[buildTenantScope, tenantScopeDescription],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const { client } = data;
|
||||
@@ -511,7 +564,7 @@ function ClientGeneralPage() {
|
||||
);
|
||||
}
|
||||
setIdTokenClaims(readIdTokenClaimsMetadata(metadata));
|
||||
}, [data]);
|
||||
}, [data, normalizeScopesForTenantAccess]);
|
||||
|
||||
const securityProfile: SecurityProfile =
|
||||
clientType === "pkce" ? "pkce" : "private";
|
||||
@@ -574,56 +627,6 @@ function ClientGeneralPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const tenantScopeDescription = t(
|
||||
"msg.dev.clients.scopes.tenant",
|
||||
"소속 테넌트 정보 접근",
|
||||
);
|
||||
|
||||
const buildTenantScope = (id: string): ScopeItem => ({
|
||||
id,
|
||||
name: "tenant",
|
||||
description: tenantScopeDescription,
|
||||
mandatory: true,
|
||||
locked: true,
|
||||
});
|
||||
|
||||
function normalizeScopesForTenantAccess(
|
||||
nextScopes: ScopeItem[],
|
||||
restricted: boolean,
|
||||
): ScopeItem[] {
|
||||
const normalized = nextScopes.map((scope) => {
|
||||
if (scope.name.trim() !== "tenant") {
|
||||
return scope;
|
||||
}
|
||||
return {
|
||||
...scope,
|
||||
description: scope.description || tenantScopeDescription,
|
||||
mandatory: restricted,
|
||||
locked: restricted,
|
||||
};
|
||||
});
|
||||
|
||||
if (
|
||||
restricted &&
|
||||
!normalized.some((scope) => scope.name.trim() === "tenant")
|
||||
) {
|
||||
normalized.push(buildTenantScope(`tenant-${Date.now()}`));
|
||||
}
|
||||
|
||||
const openidScopes = normalized.filter(
|
||||
(scope) => scope.name.trim() === "openid",
|
||||
);
|
||||
const tenantScopes = normalized.filter(
|
||||
(scope) => scope.name.trim() === "tenant",
|
||||
);
|
||||
const remainingScopes = normalized.filter((scope) => {
|
||||
const name = scope.name.trim();
|
||||
return name !== "openid" && name !== "tenant";
|
||||
});
|
||||
|
||||
return [...openidScopes, ...tenantScopes, ...remainingScopes];
|
||||
}
|
||||
|
||||
const handleTenantAccessToggle = (enabled: boolean) => {
|
||||
setTenantAccessRestricted(enabled);
|
||||
setIsTenantSearchOpen(enabled);
|
||||
@@ -2307,7 +2310,7 @@ function ClientGeneralPage() {
|
||||
</span>
|
||||
|
||||
{securityProfile === "private" && (
|
||||
<div
|
||||
<fieldset
|
||||
className="mt-4 flex items-center justify-between border-t border-primary/20 pt-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
@@ -2335,7 +2338,7 @@ function ClientGeneralPage() {
|
||||
onCheckedChange={handleHeadlessToggle}
|
||||
disabled={isGeneralSettingsReadOnly}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
</label>
|
||||
|
||||
@@ -2674,104 +2677,102 @@ function ClientGeneralPage() {
|
||||
</div>
|
||||
{currentHeadlessJwksCache.parsedKeys?.length ? (
|
||||
<div className="space-y-3">
|
||||
{currentHeadlessJwksCache.parsedKeys.map(
|
||||
(key, index) => {
|
||||
const normalizedAlgorithm = key.alg?.trim() ?? "";
|
||||
const isMissingAlgorithm =
|
||||
normalizedAlgorithm === "";
|
||||
const isUnsupportedAlgorithm =
|
||||
!isMissingAlgorithm &&
|
||||
!HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has(
|
||||
normalizedAlgorithm,
|
||||
);
|
||||
{currentHeadlessJwksCache.parsedKeys.map((key) => {
|
||||
const normalizedAlgorithm = key.alg?.trim() ?? "";
|
||||
const isMissingAlgorithm =
|
||||
normalizedAlgorithm === "";
|
||||
const isUnsupportedAlgorithm =
|
||||
!isMissingAlgorithm &&
|
||||
!HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has(
|
||||
normalizedAlgorithm,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${key.kid || "key"}-${index}`}
|
||||
className={cn(
|
||||
"rounded-xl border bg-muted/30 p-3",
|
||||
isUnsupportedAlgorithm || isMissingAlgorithm
|
||||
? "border-destructive/50 bg-destructive/5"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
KID
|
||||
</p>
|
||||
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
|
||||
{key.kid || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
KTY
|
||||
</p>
|
||||
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
|
||||
{key.kty || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
USE
|
||||
</p>
|
||||
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
|
||||
{key.use || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
ALG
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
"break-all rounded-lg border bg-background px-3 py-2 font-mono text-[11px]",
|
||||
isUnsupportedAlgorithm ||
|
||||
isMissingAlgorithm
|
||||
? "border-destructive/50 text-destructive"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
{key.alg ||
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.cache.missing_algorithm_badge",
|
||||
"알고리즘 미선언",
|
||||
)}
|
||||
</p>
|
||||
{isMissingAlgorithm && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{t(
|
||||
"msg.dev.clients.general.public_key.cache.missing_algorithm_reason",
|
||||
"이 키는 `alg`가 비어 있어서 저장할 수 없습니다.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{isUnsupportedAlgorithm && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{t(
|
||||
"msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason",
|
||||
"이 알고리즘은 Headless Login에서 지원되지 않습니다.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1">
|
||||
return (
|
||||
<div
|
||||
key={`${key.kid ?? "missing-kid"}-${key.kty ?? ""}-${key.alg ?? ""}-${key.n ?? ""}`}
|
||||
className={cn(
|
||||
"rounded-xl border bg-muted/30 p-3",
|
||||
isUnsupportedAlgorithm || isMissingAlgorithm
|
||||
? "border-destructive/50 bg-destructive/5"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.cache.parsed_key_n",
|
||||
"N",
|
||||
KID
|
||||
</p>
|
||||
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
|
||||
{key.kid || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
KTY
|
||||
</p>
|
||||
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
|
||||
{key.kty || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
USE
|
||||
</p>
|
||||
<p className="break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px]">
|
||||
{key.use || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
ALG
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
"break-all rounded-lg border bg-background px-3 py-2 font-mono text-[11px]",
|
||||
isUnsupportedAlgorithm ||
|
||||
isMissingAlgorithm
|
||||
? "border-destructive/50 text-destructive"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
{key.alg ||
|
||||
t(
|
||||
"msg.dev.clients.general.public_key.cache.missing_algorithm_badge",
|
||||
"알고리즘 미선언",
|
||||
)}
|
||||
</p>
|
||||
<p className="min-h-16 break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px] leading-5">
|
||||
{key.n || "-"}
|
||||
</p>
|
||||
{isMissingAlgorithm && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{t(
|
||||
"msg.dev.clients.general.public_key.cache.missing_algorithm_reason",
|
||||
"이 키는 `alg`가 비어 있어서 저장할 수 없습니다.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{isUnsupportedAlgorithm && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{t(
|
||||
"msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason",
|
||||
"이 알고리즘은 Headless Login에서 지원되지 않습니다.",
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
<div className="mt-3 space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.public_key.cache.parsed_key_n",
|
||||
"N",
|
||||
)}
|
||||
</p>
|
||||
<p className="min-h-16 break-all rounded-lg border border-border bg-background px-3 py-2 font-mono text-[11px] leading-5">
|
||||
{key.n || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-border px-4 py-5 text-sm text-muted-foreground">
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
} from "../../components/ui/table";
|
||||
import { toast } from "../../components/ui/use-toast";
|
||||
import {
|
||||
type DevAssignableUser,
|
||||
addClientRelation,
|
||||
type DevAssignableUser,
|
||||
fetchClient,
|
||||
fetchClientRelations,
|
||||
fetchDevUsers,
|
||||
@@ -355,7 +355,10 @@ function ClientRelationsPage() {
|
||||
</nav>
|
||||
<PageHeader
|
||||
icon={<ShieldHalf size={20} />}
|
||||
title={t("ui.dev.clients.relationships.title", "Client Relationships")}
|
||||
title={t(
|
||||
"ui.dev.clients.relationships.title",
|
||||
"Client Relationships",
|
||||
)}
|
||||
description={t(
|
||||
"msg.dev.clients.relationships.subtitle",
|
||||
"RP direct operator relation을 조회하고 User 단위로 추가·삭제합니다.",
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { BookOpenText, Filter, Plus, Search, ShieldHalf, X } from "lucide-react";
|
||||
import {
|
||||
BookOpenText,
|
||||
Filter,
|
||||
Plus,
|
||||
Search,
|
||||
ShieldHalf,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
@@ -51,8 +58,8 @@ import { Textarea } from "../../components/ui/textarea";
|
||||
import {
|
||||
type ClientSummary,
|
||||
fetchClients,
|
||||
fetchDevStats,
|
||||
fetchDeveloperRequestStatus,
|
||||
fetchDevStats,
|
||||
fetchMyTenants,
|
||||
requestDeveloperAccess,
|
||||
} from "../../lib/devApi";
|
||||
@@ -97,8 +104,7 @@ function ClientsPage() {
|
||||
} = useQuery({
|
||||
queryKey: ["developer-request", tenantId],
|
||||
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
||||
enabled:
|
||||
hasAccessToken && (role === "user" || role === "tenant_member"),
|
||||
enabled: hasAccessToken && (role === "user" || role === "tenant_member"),
|
||||
});
|
||||
const { data: tenants } = useQuery({
|
||||
queryKey: ["myTenants"],
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi";
|
||||
import {
|
||||
createIdpConfigForClient,
|
||||
listIdpConfigsForClient,
|
||||
} from "../../../lib/devApi";
|
||||
import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
|
||||
// Proper Modal Component with Form
|
||||
@@ -178,9 +178,16 @@ const CreateIdpModal = ({
|
||||
};
|
||||
|
||||
export function ClientFederationPage() {
|
||||
const { id: clientId } = useParams<{ id: string }>();
|
||||
const { id: clientIdParam } = useParams<{ id: string }>();
|
||||
const clientId = clientIdParam ?? "";
|
||||
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["idpConfigs", clientId],
|
||||
queryFn: () => listIdpConfigsForClient(clientId),
|
||||
enabled: clientId.length > 0,
|
||||
});
|
||||
|
||||
if (!clientId) {
|
||||
return (
|
||||
<div className="p-8 text-center text-destructive">
|
||||
@@ -189,11 +196,6 @@ export function ClientFederationPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["idpConfigs", clientId],
|
||||
queryFn: () => listIdpConfigsForClient(clientId),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-1">
|
||||
<PageHeader
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ClipboardCheck,
|
||||
CheckCircle2,
|
||||
ClipboardCheck,
|
||||
Clock,
|
||||
Plus,
|
||||
ShieldAlert,
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
LayoutDashboard,
|
||||
Layers3,
|
||||
LayoutDashboard,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -18,12 +18,12 @@ import {
|
||||
} from "../../../../common/core/components/overview";
|
||||
import {
|
||||
type ClientSummary,
|
||||
type RPUsageDailyMetric,
|
||||
type RPUsagePeriod,
|
||||
fetchClients,
|
||||
fetchDeveloperRequestStatus,
|
||||
fetchDevRPUsageDaily,
|
||||
fetchDevStats,
|
||||
fetchDeveloperRequestStatus,
|
||||
type RPUsageDailyMetric,
|
||||
type RPUsagePeriod,
|
||||
} from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { resolveProfileRole } from "../../lib/role";
|
||||
@@ -723,7 +723,10 @@ function GlobalOverviewPage() {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-8 items-center gap-1" aria-label="집계 단위">
|
||||
<fieldset
|
||||
className="flex h-8 items-center gap-1"
|
||||
aria-label="집계 단위"
|
||||
>
|
||||
{[
|
||||
["day", t("ui.common.chart.period.day", "일")],
|
||||
["week", t("ui.common.chart.period.week", "주")],
|
||||
@@ -743,7 +746,7 @@ function GlobalOverviewPage() {
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<OverviewSelectionChips
|
||||
|
||||
Reference in New Issue
Block a user