forked from baron/baron-sso
개발자 등록 신청 입력 안내 및 역할 표기 개선
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
ShieldHalf,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||
@@ -43,11 +43,13 @@ import {
|
||||
fetchClients,
|
||||
fetchDevStats,
|
||||
fetchDeveloperRequestStatus,
|
||||
fetchMyTenants,
|
||||
requestDeveloperAccess,
|
||||
} from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { resolveProfileRole } from "../../lib/role";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { fetchMe } from "../auth/authApi";
|
||||
|
||||
function ClientsPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -56,6 +58,7 @@ function ClientsPage() {
|
||||
const userProfile = auth.user?.profile as Record<string, unknown> | undefined;
|
||||
const role = resolveProfileRole(userProfile);
|
||||
const tenantId = userProfile?.tenant_id as string | undefined;
|
||||
const companyCode = userProfile?.companyCode as string | undefined;
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -82,6 +85,16 @@ function ClientsPage() {
|
||||
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
||||
enabled: hasAccessToken && role === "user",
|
||||
});
|
||||
const { data: tenants } = useQuery({
|
||||
queryKey: ["myTenants"],
|
||||
queryFn: fetchMyTenants,
|
||||
enabled: hasAccessToken,
|
||||
});
|
||||
const { data: me } = useQuery({
|
||||
queryKey: ["userMe"],
|
||||
queryFn: fetchMe,
|
||||
enabled: hasAccessToken,
|
||||
});
|
||||
|
||||
const canCreateClient =
|
||||
(role !== "user" && role !== "tenant_member") ||
|
||||
@@ -117,6 +130,19 @@ function ClientsPage() {
|
||||
const authFailures = statsData?.auth_failures_24h ?? 0;
|
||||
const hasFilterResult = filteredClients.length > 0;
|
||||
const isFilteredOut = clients.length > 0 && !hasFilterResult;
|
||||
const currentTenant = tenants?.find(
|
||||
(tenant) => tenant.id === tenantId || tenant.slug === companyCode,
|
||||
);
|
||||
const organizationName = currentTenant?.name || companyCode || "";
|
||||
const profileName = me?.name || (userProfile?.name as string) || "";
|
||||
const profileEmail = me?.email || (userProfile?.email as string) || "";
|
||||
const profilePhone =
|
||||
me?.phone ||
|
||||
(userProfile?.phone as string | undefined) ||
|
||||
(userProfile?.phone_number as string | undefined) ||
|
||||
"";
|
||||
const profileRole = me?.role || role;
|
||||
const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole);
|
||||
|
||||
type StatTone = "up" | "down" | "stable";
|
||||
type StatItem = {
|
||||
@@ -644,8 +670,11 @@ function ClientsPage() {
|
||||
setIsRequestModalOpen(false);
|
||||
}}
|
||||
tenantId={tenantId || ""}
|
||||
initialName={(userProfile?.name as string) || ""}
|
||||
initialOrg={(userProfile?.companyCode as string) || ""}
|
||||
initialName={profileName}
|
||||
initialOrg={organizationName}
|
||||
initialEmail={profileEmail}
|
||||
initialPhone={profilePhone}
|
||||
initialRole={profileRoleLabel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -658,6 +687,9 @@ interface RequestAccessModalProps {
|
||||
tenantId: string;
|
||||
initialName: string;
|
||||
initialOrg: string;
|
||||
initialEmail: string;
|
||||
initialPhone: string;
|
||||
initialRole: string;
|
||||
}
|
||||
|
||||
function RequestAccessModal({
|
||||
@@ -667,11 +699,20 @@ function RequestAccessModal({
|
||||
tenantId,
|
||||
initialName,
|
||||
initialOrg,
|
||||
initialEmail,
|
||||
initialPhone,
|
||||
initialRole,
|
||||
}: RequestAccessModalProps) {
|
||||
const [name, setName] = useState(initialName);
|
||||
const [organization, setOrganization] = useState(initialOrg);
|
||||
const [reason, setReason] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setName(initialName);
|
||||
setOrganization(initialOrg);
|
||||
}, [initialName, initialOrg, isOpen]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: requestDeveloperAccess,
|
||||
onSuccess: () => {
|
||||
@@ -725,7 +766,8 @@ function RequestAccessModal({
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -736,13 +778,50 @@ function RequestAccessModal({
|
||||
<Input
|
||||
id="org"
|
||||
value={organization}
|
||||
onChange={(e) => setOrganization(e.target.value)}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">
|
||||
{t("ui.dev.request.modal.email", "이메일")}
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
value={initialEmail}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone">
|
||||
{t("ui.dev.request.modal.phone", "전화번호")}
|
||||
</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={initialPhone}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="role">
|
||||
{t("ui.dev.request.modal.role", "역할")}
|
||||
</Label>
|
||||
<Input
|
||||
id="role"
|
||||
value={initialRole}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="reason">
|
||||
{t("ui.dev.request.modal.reason", "신청 사유")}
|
||||
{t("ui.dev.request.modal.reason", "신청 사유")}{" "}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="reason"
|
||||
@@ -752,7 +831,7 @@ function RequestAccessModal({
|
||||
"ui.dev.request.modal.reason_placeholder",
|
||||
"예: 자체 서비스 연동 및 테스트용 OIDC 클라이언트 생성이 필요합니다.",
|
||||
)}
|
||||
className="min-h-[120px] resize-none"
|
||||
className="min-h-[120px] resize-none border-primary/50 bg-background focus-visible:ring-primary/40"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
X,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
@@ -32,11 +32,13 @@ import {
|
||||
approveDeveloperRequest,
|
||||
cancelDeveloperRequestApproval,
|
||||
fetchDeveloperRequests,
|
||||
fetchMyTenants,
|
||||
rejectDeveloperRequest,
|
||||
requestDeveloperAccess,
|
||||
} from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { resolveProfileRole } from "../../lib/role";
|
||||
import { fetchMe } from "../auth/authApi";
|
||||
|
||||
export default function DeveloperRequestPage() {
|
||||
const auth = useAuth();
|
||||
@@ -45,6 +47,7 @@ export default function DeveloperRequestPage() {
|
||||
const role = resolveProfileRole(userProfile);
|
||||
const isSuperAdmin = role === "super_admin";
|
||||
const tenantId = userProfile?.tenant_id as string | undefined;
|
||||
const companyCode = userProfile?.companyCode as string | undefined;
|
||||
|
||||
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
||||
const [adminNotes, setAdminNotes] = useState<Record<number, string>>({});
|
||||
@@ -54,6 +57,30 @@ export default function DeveloperRequestPage() {
|
||||
queryFn: () => fetchDeveloperRequests(),
|
||||
enabled: !!auth.user?.access_token,
|
||||
});
|
||||
const { data: tenants } = useQuery({
|
||||
queryKey: ["myTenants"],
|
||||
queryFn: fetchMyTenants,
|
||||
enabled: !!auth.user?.access_token,
|
||||
});
|
||||
const { data: me } = useQuery({
|
||||
queryKey: ["userMe"],
|
||||
queryFn: fetchMe,
|
||||
enabled: !!auth.user?.access_token,
|
||||
});
|
||||
|
||||
const currentTenant = tenants?.find(
|
||||
(tenant) => tenant.id === tenantId || tenant.slug === companyCode,
|
||||
);
|
||||
const organizationName = currentTenant?.name || companyCode || "";
|
||||
const profileName = me?.name || (userProfile?.name as string) || "";
|
||||
const profileEmail = me?.email || (userProfile?.email as string) || "";
|
||||
const profilePhone =
|
||||
me?.phone ||
|
||||
(userProfile?.phone as string | undefined) ||
|
||||
(userProfile?.phone_number as string | undefined) ||
|
||||
"";
|
||||
const profileRole = me?.role || role;
|
||||
const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole);
|
||||
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: ({ id, adminNotes }: { id: number; adminNotes: string }) =>
|
||||
@@ -189,8 +216,13 @@ export default function DeveloperRequestPage() {
|
||||
<TableCell className="font-medium">
|
||||
<div>{req.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{req.userId}
|
||||
{req.email || req.userId}
|
||||
</div>
|
||||
{(req.phone || req.role) && (
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{[req.phone, req.role].filter(Boolean).join(" / ")}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{req.organization}</TableCell>
|
||||
@@ -302,8 +334,11 @@ export default function DeveloperRequestPage() {
|
||||
setIsRequestModalOpen(false);
|
||||
}}
|
||||
tenantId={tenantId || ""}
|
||||
initialName={(userProfile?.name as string) || ""}
|
||||
initialOrg={(userProfile?.companyCode as string) || ""}
|
||||
initialName={profileName}
|
||||
initialOrg={organizationName}
|
||||
initialEmail={profileEmail}
|
||||
initialPhone={profilePhone}
|
||||
initialRole={profileRoleLabel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -352,6 +387,9 @@ interface RequestAccessModalProps {
|
||||
tenantId: string;
|
||||
initialName: string;
|
||||
initialOrg: string;
|
||||
initialEmail: string;
|
||||
initialPhone: string;
|
||||
initialRole: string;
|
||||
}
|
||||
|
||||
function RequestAccessModal({
|
||||
@@ -361,11 +399,20 @@ function RequestAccessModal({
|
||||
tenantId,
|
||||
initialName,
|
||||
initialOrg,
|
||||
initialEmail,
|
||||
initialPhone,
|
||||
initialRole,
|
||||
}: RequestAccessModalProps) {
|
||||
const [name, setName] = useState(initialName);
|
||||
const [organization, setOrganization] = useState(initialOrg);
|
||||
const [reason, setReason] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setName(initialName);
|
||||
setOrganization(initialOrg);
|
||||
}, [initialName, initialOrg, isOpen]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: requestDeveloperAccess,
|
||||
onSuccess: () => {
|
||||
@@ -419,7 +466,8 @@ function RequestAccessModal({
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -430,13 +478,50 @@ function RequestAccessModal({
|
||||
<Input
|
||||
id="org"
|
||||
value={organization}
|
||||
onChange={(e) => setOrganization(e.target.value)}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">
|
||||
{t("ui.dev.request.modal.email", "이메일")}
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
value={initialEmail}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone">
|
||||
{t("ui.dev.request.modal.phone", "전화번호")}
|
||||
</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={initialPhone}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="role">
|
||||
{t("ui.dev.request.modal.role", "역할")}
|
||||
</Label>
|
||||
<Input
|
||||
id="role"
|
||||
value={initialRole}
|
||||
readOnly
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="reason">
|
||||
{t("ui.dev.request.modal.reason", "신청 사유")}
|
||||
{t("ui.dev.request.modal.reason", "신청 사유")}{" "}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="reason"
|
||||
@@ -446,7 +531,7 @@ function RequestAccessModal({
|
||||
"ui.dev.request.modal.reason_placeholder",
|
||||
"예: 자체 서비스 연동 및 테스트용 OIDC 클라이언트 생성이 필요합니다.",
|
||||
)}
|
||||
className="min-h-[120px] resize-none"
|
||||
className="min-h-[120px] resize-none border-primary/50 bg-background focus-visible:ring-primary/40"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -401,6 +401,9 @@ export type DeveloperRequest = {
|
||||
tenantId: string;
|
||||
name: string;
|
||||
organization: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
role?: string;
|
||||
reason: string;
|
||||
status: DeveloperRequestStatus;
|
||||
adminNotes?: string;
|
||||
|
||||
Reference in New Issue
Block a user