1
0
forked from baron/baron-sso

ci: add code check badges and coverage reports

This commit is contained in:
2026-05-29 12:05:43 +09:00
parent c489c7c38f
commit a830242947
164 changed files with 9059 additions and 2012 deletions

View File

@@ -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,

View File

@@ -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">

View File

@@ -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 단위로 추가·삭제합니다.",

View File

@@ -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"],

View File

@@ -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

View File

@@ -1,7 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
ClipboardCheck,
CheckCircle2,
ClipboardCheck,
Clock,
Plus,
ShieldAlert,

View File

@@ -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