forked from baron/baron-sso
린트 적용
This commit is contained in:
@@ -100,7 +100,12 @@ const RoleSwitcher: FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(
|
{(
|
||||||
["super_admin", "tenant_admin", "rp_admin", "tenant_member"] as const
|
[
|
||||||
|
"super_admin",
|
||||||
|
"tenant_admin",
|
||||||
|
"rp_admin",
|
||||||
|
"tenant_member",
|
||||||
|
] as const
|
||||||
).map((role) => (
|
).map((role) => (
|
||||||
<button
|
<button
|
||||||
key={role}
|
key={role}
|
||||||
@@ -120,8 +125,12 @@ const RoleSwitcher: FC = () => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{roleLabels[role] ?? role.toUpperCase().replace("_", " ")}</span>
|
<span>
|
||||||
{currentRole === role && <span style={{ marginLeft: "8px" }}>✅</span>}
|
{roleLabels[role] ?? role.toUpperCase().replace("_", " ")}
|
||||||
|
</span>
|
||||||
|
{currentRole === role && (
|
||||||
|
<span style={{ marginLeft: "8px" }}>✅</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ const badgeVariants = cva(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
extends
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
VariantProps<typeof badgeVariants> {}
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { Button } from "./button";
|
|||||||
describe("Button Component", () => {
|
describe("Button Component", () => {
|
||||||
it("renders correctly with children", () => {
|
it("renders correctly with children", () => {
|
||||||
render(<Button>Click me</Button>);
|
render(<Button>Click me</Button>);
|
||||||
expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByRole("button", { name: /click me/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies variant classes correctly", () => {
|
it("applies variant classes correctly", () => {
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ const buttonVariants = cva(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends
|
||||||
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
export interface TextareaProps
|
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import { Plus, Search, ShieldCheck, Trash2, UserPlus, Users } from "lucide-react";
|
import {
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
ShieldCheck,
|
||||||
|
Trash2,
|
||||||
|
UserPlus,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -64,11 +71,16 @@ export function TenantAdminsTab() {
|
|||||||
mutationFn: (userId: string) => addTenantAdmin(tenantId, userId),
|
mutationFn: (userId: string) => addTenantAdmin(tenantId, userId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] });
|
queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] });
|
||||||
toast.success(t("msg.admin.tenants.admins.add_success", "관리자가 추가되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.tenants.admins.add_success", "관리자가 추가되었습니다."),
|
||||||
|
);
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>) => {
|
onError: (err: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
toast.error(
|
||||||
|
err.response?.data?.error ||
|
||||||
|
t("msg.common.error", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,10 +88,15 @@ export function TenantAdminsTab() {
|
|||||||
mutationFn: (userId: string) => removeTenantAdmin(tenantId, userId),
|
mutationFn: (userId: string) => removeTenantAdmin(tenantId, userId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] });
|
queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] });
|
||||||
toast.success(t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>) => {
|
onError: (err: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
toast.error(
|
||||||
|
err.response?.data?.error ||
|
||||||
|
t("msg.common.error", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,7 +105,15 @@ export function TenantAdminsTab() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveAdmin = (userId: string, userName: string) => {
|
const handleRemoveAdmin = (userId: string, userName: string) => {
|
||||||
if (window.confirm(t("msg.admin.tenants.admins.remove_confirm", "관리자를 삭제하시겠습니까?", { name: userName }))) {
|
if (
|
||||||
|
window.confirm(
|
||||||
|
t(
|
||||||
|
"msg.admin.tenants.admins.remove_confirm",
|
||||||
|
"관리자를 삭제하시겠습니까?",
|
||||||
|
{ name: userName },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
removeMutation.mutate(userId);
|
removeMutation.mutate(userId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -106,14 +131,20 @@ export function TenantAdminsTab() {
|
|||||||
{t("ui.admin.tenants.admins.title", "테넌트 관리자")}
|
{t("ui.admin.tenants.admins.title", "테넌트 관리자")}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-muted-foreground">
|
<CardDescription className="text-muted-foreground">
|
||||||
{t("msg.admin.tenants.admins.subtitle", "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.")}
|
{t(
|
||||||
|
"msg.admin.tenants.admins.subtitle",
|
||||||
|
"이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.",
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={isDialogOpen} onOpenChange={(open) => {
|
<Dialog
|
||||||
setIsAddDialogOpen(open);
|
open={isDialogOpen}
|
||||||
if (!open) setSearchTerm("");
|
onOpenChange={(open) => {
|
||||||
}}>
|
setIsAddDialogOpen(open);
|
||||||
|
if (!open) setSearchTerm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">
|
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">
|
||||||
<UserPlus className="mr-2 h-4 w-4" />
|
<UserPlus className="mr-2 h-4 w-4" />
|
||||||
@@ -126,7 +157,10 @@ export function TenantAdminsTab() {
|
|||||||
{t("ui.admin.tenants.admins.dialog_title", "새 관리자 추가")}
|
{t("ui.admin.tenants.admins.dialog_title", "새 관리자 추가")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("ui.admin.tenants.admins.dialog_description", "이름 또는 이메일로 사용자를 검색하세요.")}
|
{t(
|
||||||
|
"ui.admin.tenants.admins.dialog_description",
|
||||||
|
"이름 또는 이메일로 사용자를 검색하세요.",
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -134,7 +168,10 @@ export function TenantAdminsTab() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("ui.admin.tenants.admins.dialog_search_placeholder", "사용자 검색 (최소 2자)...")}
|
placeholder={t(
|
||||||
|
"ui.admin.tenants.admins.dialog_search_placeholder",
|
||||||
|
"사용자 검색 (최소 2자)...",
|
||||||
|
)}
|
||||||
className="pl-10 h-11"
|
className="pl-10 h-11"
|
||||||
autoFocus
|
autoFocus
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
@@ -146,7 +183,12 @@ export function TenantAdminsTab() {
|
|||||||
{searchTerm.length < 2 ? (
|
{searchTerm.length < 2 ? (
|
||||||
<div className="p-10 text-center text-muted-foreground flex flex-col items-center gap-2">
|
<div className="p-10 text-center text-muted-foreground flex flex-col items-center gap-2">
|
||||||
<Search className="h-8 w-8 opacity-20" />
|
<Search className="h-8 w-8 opacity-20" />
|
||||||
<p className="text-sm">{t("ui.admin.tenants.admins.dialog_search_hint", "검색어를 입력해 주세요.")}</p>
|
<p className="text-sm">
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.admins.dialog_search_hint",
|
||||||
|
"검색어를 입력해 주세요.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : usersQuery.isLoading ? (
|
) : usersQuery.isLoading ? (
|
||||||
<div className="p-10 text-center">
|
<div className="p-10 text-center">
|
||||||
@@ -154,21 +196,33 @@ export function TenantAdminsTab() {
|
|||||||
</div>
|
</div>
|
||||||
) : searchResults.length === 0 ? (
|
) : searchResults.length === 0 ? (
|
||||||
<div className="p-10 text-center text-muted-foreground">
|
<div className="p-10 text-center text-muted-foreground">
|
||||||
{t("ui.admin.tenants.admins.dialog_no_results", "검색 결과가 없습니다.")}
|
{t(
|
||||||
|
"ui.admin.tenants.admins.dialog_no_results",
|
||||||
|
"검색 결과가 없습니다.",
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{searchResults.map((user) => {
|
{searchResults.map((user) => {
|
||||||
const isAlreadyAdmin = currentAdmins.some((a) => a.id === user.id);
|
const isAlreadyAdmin = currentAdmins.some(
|
||||||
|
(a) => a.id === user.id,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div key={user.id} className="flex items-center justify-between p-3 hover:bg-muted/50 transition-colors">
|
<div
|
||||||
|
key={user.id}
|
||||||
|
className="flex items-center justify-between p-3 hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-9 w-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xs">
|
<div className="h-9 w-9 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xs">
|
||||||
{user.name.charAt(0)}
|
{user.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium">{user.name}</span>
|
<span className="text-sm font-medium">
|
||||||
<span className="text-xs text-muted-foreground">{user.email}</span>
|
{user.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -178,9 +232,20 @@ export function TenantAdminsTab() {
|
|||||||
onClick={() => handleAddAdmin(user.id)}
|
onClick={() => handleAddAdmin(user.id)}
|
||||||
>
|
>
|
||||||
{isAlreadyAdmin ? (
|
{isAlreadyAdmin ? (
|
||||||
<Badge variant="secondary" className="font-normal">{t("ui.admin.tenants.admins.already_admin", "이미 관리자")}</Badge>
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="font-normal"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"ui.admin.tenants.admins.already_admin",
|
||||||
|
"이미 관리자",
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<><Plus className="h-3 w-3 mr-1" /> {t("ui.common.add", "추가")}</>
|
<>
|
||||||
|
<Plus className="h-3 w-3 mr-1" />{" "}
|
||||||
|
{t("ui.common.add", "추가")}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,16 +284,27 @@ export function TenantAdminsTab() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
) : currentAdmins.length === 0 ? (
|
) : currentAdmins.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="h-32 text-center text-muted-foreground">
|
<TableCell
|
||||||
|
colSpan={3}
|
||||||
|
className="h-32 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Users className="h-8 w-8 opacity-20" />
|
<Users className="h-8 w-8 opacity-20" />
|
||||||
<p>{t("msg.admin.tenants.admins.empty", "등록된 관리자가 없습니다.")}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
"msg.admin.tenants.admins.empty",
|
||||||
|
"등록된 관리자가 없습니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
currentAdmins.map((admin) => (
|
currentAdmins.map((admin) => (
|
||||||
<TableRow key={admin.id} className="hover:bg-muted/30 transition-colors group">
|
<TableRow
|
||||||
|
key={admin.id}
|
||||||
|
className="hover:bg-muted/30 transition-colors group"
|
||||||
|
>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-8 w-8 rounded-lg bg-secondary flex items-center justify-center text-secondary-foreground font-bold text-xs">
|
<div className="h-8 w-8 rounded-lg bg-secondary flex items-center justify-center text-secondary-foreground font-bold text-xs">
|
||||||
@@ -245,9 +321,14 @@ export function TenantAdminsTab() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="opacity-0 group-hover:opacity-100 text-destructive hover:text-destructive hover:bg-destructive/10 transition-all"
|
className="opacity-0 group-hover:opacity-100 text-destructive hover:text-destructive hover:bg-destructive/10 transition-all"
|
||||||
onClick={() => handleRemoveAdmin(admin.id, admin.name)}
|
onClick={() =>
|
||||||
|
handleRemoveAdmin(admin.id, admin.name)
|
||||||
|
}
|
||||||
disabled={removeMutation.isPending}
|
disabled={removeMutation.isPending}
|
||||||
title={t("ui.admin.tenants.admins.remove_title", "관리자 권한 회수")}
|
title={t(
|
||||||
|
"ui.admin.tenants.admins.remove_title",
|
||||||
|
"관리자 권한 회수",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -117,10 +117,27 @@ function TenantCreatePage() {
|
|||||||
value={type}
|
value={type}
|
||||||
onChange={(e) => setType(e.target.value)}
|
onChange={(e) => setType(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="COMPANY">{t("domain.tenant_type.company", "COMPANY (일반 기업)")}</option>
|
<option value="COMPANY">
|
||||||
<option value="COMPANY_GROUP">{t("domain.tenant_type.company_group", "COMPANY_GROUP (그룹사/지주사)")}</option>
|
{t("domain.tenant_type.company", "COMPANY (일반 기업)")}
|
||||||
<option value="USER_GROUP">{t("domain.tenant_type.user_group", "USER_GROUP (내부 부서/팀)")}</option>
|
</option>
|
||||||
<option value="PERSONAL">{t("domain.tenant_type.personal", "PERSONAL (개인 워크스페이스)")}</option>
|
<option value="COMPANY_GROUP">
|
||||||
|
{t(
|
||||||
|
"domain.tenant_type.company_group",
|
||||||
|
"COMPANY_GROUP (그룹사/지주사)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
<option value="USER_GROUP">
|
||||||
|
{t(
|
||||||
|
"domain.tenant_type.user_group",
|
||||||
|
"USER_GROUP (내부 부서/팀)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
<option value="PERSONAL">
|
||||||
|
{t(
|
||||||
|
"domain.tenant_type.personal",
|
||||||
|
"PERSONAL (개인 워크스페이스)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -25,21 +25,32 @@ function TenantDetailPage() {
|
|||||||
<header className="flex flex-wrap items-start justify-between gap-4">
|
<header className="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
||||||
<Link to="/tenants" className="inline-flex items-center gap-2 hover:text-foreground transition-colors">
|
<Link
|
||||||
|
to="/tenants"
|
||||||
|
className="inline-flex items-center gap-2 hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
<ArrowLeft size={14} />
|
<ArrowLeft size={14} />
|
||||||
{t("ui.admin.tenants.detail.breadcrumb_list", "테넌트 목록")}
|
{t("ui.admin.tenants.detail.breadcrumb_list", "테넌트 목록")}
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span className="text-foreground">{t("ui.admin.tenants.detail.title", "상세")}</span>
|
<span className="text-foreground">
|
||||||
|
{t("ui.admin.tenants.detail.title", "상세")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-semibold">
|
<h2 className="text-3xl font-semibold">
|
||||||
{tenantQuery.data?.name ?? t("ui.admin.tenants.detail.loading", "불러오는 중...")}
|
{tenantQuery.data?.name ??
|
||||||
|
t("ui.admin.tenants.detail.loading", "불러오는 중...")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-[var(--color-muted)]">
|
<p className="text-sm text-[var(--color-muted)]">
|
||||||
{t("ui.admin.tenants.detail.header_subtitle", "테넌트 정보를 수정하거나 연동 설정을 관리합니다.")}
|
{t(
|
||||||
|
"ui.admin.tenants.detail.header_subtitle",
|
||||||
|
"테넌트 정보를 수정하거나 연동 설정을 관리합니다.",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="muted">{t("ui.common.admin_only", "관리자 전용")}</Badge>
|
<Badge variant="muted">
|
||||||
|
{t("ui.common.admin_only", "관리자 전용")}
|
||||||
|
</Badge>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
|
|||||||
@@ -43,9 +43,15 @@ import {
|
|||||||
import { t } from "../../../lib/i18n";
|
import { t } from "../../../lib/i18n";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type UserGroupNode = GroupSummary & { children: UserGroupNode[]; isExpanded?: boolean };
|
type UserGroupNode = GroupSummary & {
|
||||||
|
children: UserGroupNode[];
|
||||||
|
isExpanded?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
function buildGroupTree(groups: GroupSummary[], parentId: string | null = null): UserGroupNode[] {
|
function buildGroupTree(
|
||||||
|
groups: GroupSummary[],
|
||||||
|
parentId: string | null = null,
|
||||||
|
): UserGroupNode[] {
|
||||||
const nodes: UserGroupNode[] = [];
|
const nodes: UserGroupNode[] = [];
|
||||||
const childrenOf = new Map<string, UserGroupNode[]>();
|
const childrenOf = new Map<string, UserGroupNode[]>();
|
||||||
|
|
||||||
@@ -56,7 +62,10 @@ function buildGroupTree(groups: GroupSummary[], parentId: string | null = null):
|
|||||||
|
|
||||||
// Second pass: Populate children
|
// Second pass: Populate children
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
const node: UserGroupNode = { ...group, children: childrenOf.get(group.id)! };
|
const node: UserGroupNode = {
|
||||||
|
...group,
|
||||||
|
children: childrenOf.get(group.id)!,
|
||||||
|
};
|
||||||
if (group.parentId === parentId) {
|
if (group.parentId === parentId) {
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
@@ -73,7 +82,7 @@ function buildGroupTree(groups: GroupSummary[], parentId: string | null = null):
|
|||||||
|
|
||||||
// Sort children for consistent rendering (optional, but good for UI)
|
// Sort children for consistent rendering (optional, but good for UI)
|
||||||
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
nodes.forEach(node => {
|
nodes.forEach((node) => {
|
||||||
node.children.sort((a, b) => a.name.localeCompare(b.name));
|
node.children.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,11 +139,16 @@ const UserGroupTreeNode: React.FC<UserGroupTreeNodeProps> = ({
|
|||||||
<ChevronRight size={16} />
|
<ChevronRight size={16} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
) : (level > 0 && (
|
) : (
|
||||||
|
level > 0 && (
|
||||||
<span className="inline-block w-6 text-center">
|
<span className="inline-block w-6 text-center">
|
||||||
<ChevronRight size={16} className="text-muted-foreground inline-block align-middle" />
|
<ChevronRight
|
||||||
|
size={16}
|
||||||
|
className="text-muted-foreground inline-block align-middle"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
<Users size={14} className="text-muted-foreground" />
|
<Users size={14} className="text-muted-foreground" />
|
||||||
<span className="font-semibold">{node.name}</span>
|
<span className="font-semibold">{node.name}</span>
|
||||||
<Badge variant="secondary" className="text-[10px] font-mono">
|
<Badge variant="secondary" className="text-[10px] font-mono">
|
||||||
@@ -221,7 +235,12 @@ function TenantGroupsPage() {
|
|||||||
parentId: newGroupParentId || undefined,
|
parentId: newGroupParentId || undefined,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("msg.admin.groups.list.create_success", "그룹이 성공적으로 생성되었습니다."));
|
toast.success(
|
||||||
|
t(
|
||||||
|
"msg.admin.groups.list.create_success",
|
||||||
|
"그룹이 성공적으로 생성되었습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
groupsQuery.refetch();
|
groupsQuery.refetch();
|
||||||
setNewGroupName("");
|
setNewGroupName("");
|
||||||
setNewGroupNameDesc("");
|
setNewGroupNameDesc("");
|
||||||
@@ -229,21 +248,27 @@ function TenantGroupsPage() {
|
|||||||
setNewGroupParentId(null);
|
setNewGroupParentId(null);
|
||||||
},
|
},
|
||||||
onError: (error: AxiosError<{ error?: string }>) => {
|
onError: (error: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(t("msg.admin.groups.list.create_error", "그룹 생성 실패"), { description: error.response?.data?.error || error.message });
|
toast.error(t("msg.admin.groups.list.create_error", "그룹 생성 실패"), {
|
||||||
}
|
description: error.response?.data?.error || error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 그룹 삭제
|
// 그룹 삭제
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: (id: string) => deleteGroup(tenantId, id),
|
mutationFn: (id: string) => deleteGroup(tenantId, id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("msg.admin.groups.list.delete_success", "그룹이 삭제되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.groups.list.delete_success", "그룹이 삭제되었습니다."),
|
||||||
|
);
|
||||||
groupsQuery.refetch();
|
groupsQuery.refetch();
|
||||||
setSelectedGroupId(null);
|
setSelectedGroupId(null);
|
||||||
},
|
},
|
||||||
onError: (error: AxiosError<{ error?: string }>) => {
|
onError: (error: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(t("msg.admin.groups.list.delete_error", "그룹 삭제 실패"), { description: error.response?.data?.error || error.message });
|
toast.error(t("msg.admin.groups.list.delete_error", "그룹 삭제 실패"), {
|
||||||
}
|
description: error.response?.data?.error || error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 멤버 추가
|
// 멤버 추가
|
||||||
@@ -251,12 +276,16 @@ function TenantGroupsPage() {
|
|||||||
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
|
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
|
||||||
addGroupMember(tenantId, groupId, userId),
|
addGroupMember(tenantId, groupId, userId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("msg.admin.groups.members.add_success", "멤버가 추가되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.groups.members.add_success", "멤버가 추가되었습니다."),
|
||||||
|
);
|
||||||
groupsQuery.refetch();
|
groupsQuery.refetch();
|
||||||
},
|
},
|
||||||
onError: (error: AxiosError<{ error?: string }>) => {
|
onError: (error: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(t("msg.common.error", "오류 발생"), { description: error.response?.data?.error || error.message });
|
toast.error(t("msg.common.error", "오류 발생"), {
|
||||||
}
|
description: error.response?.data?.error || error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 멤버 제거
|
// 멤버 제거
|
||||||
@@ -264,15 +293,21 @@ function TenantGroupsPage() {
|
|||||||
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
|
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
|
||||||
removeGroupMember(tenantId, groupId, userId),
|
removeGroupMember(tenantId, groupId, userId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("msg.admin.groups.members.remove_success", "멤버가 제거되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.groups.members.remove_success", "멤버가 제거되었습니다."),
|
||||||
|
);
|
||||||
groupsQuery.refetch();
|
groupsQuery.refetch();
|
||||||
},
|
},
|
||||||
onError: (error: AxiosError<{ error?: string }>) => {
|
onError: (error: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(t("msg.common.error", "오류 발생"), { description: error.response?.data?.error || error.message });
|
toast.error(t("msg.common.error", "오류 발생"), {
|
||||||
}
|
description: error.response?.data?.error || error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupTree = groupsQuery.data ? buildGroupTree(groupsQuery.data, tenantId) : [];
|
const groupTree = groupsQuery.data
|
||||||
|
? buildGroupTree(groupsQuery.data, tenantId)
|
||||||
|
: [];
|
||||||
|
|
||||||
const handleAddSubGroup = (parentId: string) => {
|
const handleAddSubGroup = (parentId: string) => {
|
||||||
setNewGroupParentId(parentId);
|
setNewGroupParentId(parentId);
|
||||||
@@ -304,7 +339,10 @@ function TenantGroupsPage() {
|
|||||||
{t("ui.admin.groups.create.title", "새 그룹 생성")}
|
{t("ui.admin.groups.create.title", "새 그룹 생성")}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t("ui.admin.groups.create.description", "새로운 사용자 그룹을 생성하고 계층 구조를 설정합니다.")}
|
{t(
|
||||||
|
"ui.admin.groups.create.description",
|
||||||
|
"새로운 사용자 그룹을 생성하고 계층 구조를 설정합니다.",
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
@@ -425,8 +463,14 @@ function TenantGroupsPage() {
|
|||||||
)}
|
)}
|
||||||
{!groupsQuery.isLoading && groupTree.length === 0 && (
|
{!groupsQuery.isLoading && groupTree.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="text-center py-8 text-muted-foreground">
|
<TableCell
|
||||||
{t("msg.admin.groups.list.empty", "아직 등록된 그룹이 없습니다.")}
|
colSpan={3}
|
||||||
|
className="text-center py-8 text-muted-foreground"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"msg.admin.groups.list.empty",
|
||||||
|
"아직 등록된 그룹이 없습니다.",
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@@ -438,9 +482,16 @@ function TenantGroupsPage() {
|
|||||||
onSelect={setSelectedGroupId}
|
onSelect={setSelectedGroupId}
|
||||||
selectedGroupId={selectedGroupId}
|
selectedGroupId={selectedGroupId}
|
||||||
onDelete={(id) => {
|
onDelete={(id) => {
|
||||||
if (window.confirm(t("msg.admin.groups.list.delete_confirm", "그룹을 삭제하시겠습니까?"))) {
|
if (
|
||||||
deleteMutation.mutate(id);
|
window.confirm(
|
||||||
}
|
t(
|
||||||
|
"msg.admin.groups.list.delete_confirm",
|
||||||
|
"그룹을 삭제하시겠습니까?",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deleteMutation.mutate(id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onAddSubGroup={handleAddSubGroup}
|
onAddSubGroup={handleAddSubGroup}
|
||||||
addMemberMutation={addMemberMutation}
|
addMemberMutation={addMemberMutation}
|
||||||
@@ -464,15 +515,22 @@ function TenantGroupsPage() {
|
|||||||
})}
|
})}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t("ui.admin.groups.detail.members_subtitle", "그룹에 속한 멤버들을 확인하고 관리합니다.")}
|
{t(
|
||||||
|
"ui.admin.groups.detail.members_subtitle",
|
||||||
|
"그룹에 속한 멤버들을 확인하고 관리합니다.",
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex justify-end mb-4">
|
<div className="flex justify-end mb-4">
|
||||||
<Button size="sm" onClick={() => handleAddMember(currentGroup.id)} disabled={addMemberMutation.isPending}>
|
<Button
|
||||||
<UserPlus size={14} className="mr-1" />
|
size="sm"
|
||||||
{t("ui.common.add", "멤버 추가")}
|
onClick={() => handleAddMember(currentGroup.id)}
|
||||||
</Button>
|
disabled={addMemberMutation.isPending}
|
||||||
|
>
|
||||||
|
<UserPlus size={14} className="mr-1" />
|
||||||
|
{t("ui.common.add", "멤버 추가")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import {
|
import { CornerDownRight, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||||
CornerDownRight,
|
|
||||||
Pencil,
|
|
||||||
Plus,
|
|
||||||
RefreshCw,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Badge } from "../../../components/ui/badge";
|
import { Badge } from "../../../components/ui/badge";
|
||||||
@@ -72,7 +66,9 @@ const TenantRow: React.FC<{
|
|||||||
<TableRow key={tenant.id}>
|
<TableRow key={tenant.id}>
|
||||||
<TableCell style={{ paddingLeft: `${1 + level * 1.5}rem` }}>
|
<TableCell style={{ paddingLeft: `${1 + level * 1.5}rem` }}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{level > 0 && <CornerDownRight size={14} className="text-muted-foreground" />}
|
{level > 0 && (
|
||||||
|
<CornerDownRight size={14} className="text-muted-foreground" />
|
||||||
|
)}
|
||||||
<span className="font-semibold">{tenant.name}</span>
|
<span className="font-semibold">{tenant.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -88,8 +84,8 @@ const TenantRow: React.FC<{
|
|||||||
tenant.status === "active"
|
tenant.status === "active"
|
||||||
? "default"
|
? "default"
|
||||||
: tenant.status === "pending"
|
: tenant.status === "pending"
|
||||||
? "secondary"
|
? "secondary"
|
||||||
: "muted"
|
: "muted"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(`ui.common.status.${tenant.status}`, tenant.status)}
|
{t(`ui.common.status.${tenant.status}`, tenant.status)}
|
||||||
@@ -267,7 +263,10 @@ function TenantListPage() {
|
|||||||
)}
|
)}
|
||||||
{!query.isLoading && tenantTree.length === 0 && (
|
{!query.isLoading && tenantTree.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
<TableCell
|
||||||
|
colSpan={6}
|
||||||
|
className="text-center py-8 text-muted-foreground"
|
||||||
|
>
|
||||||
{t(
|
{t(
|
||||||
"msg.admin.tenants.empty",
|
"msg.admin.tenants.empty",
|
||||||
"아직 등록된 테넌트가 없습니다.",
|
"아직 등록된 테넌트가 없습니다.",
|
||||||
@@ -293,4 +292,3 @@ function TenantListPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default TenantListPage;
|
export default TenantListPage;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export function TenantProfilePage() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
return <div>{t("msg.admin.tenants.missing_id", "테넌트 ID가 없습니다.")}</div>;
|
return (
|
||||||
|
<div>{t("msg.admin.tenants.missing_id", "테넌트 ID가 없습니다.")}</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenantQuery = useQuery({
|
const tenantQuery = useQuery({
|
||||||
@@ -74,8 +76,11 @@ export function TenantProfilePage() {
|
|||||||
toast.success(t("msg.info.saved_success", "저장되었습니다."));
|
toast.success(t("msg.info.saved_success", "저장되었습니다."));
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>) => {
|
onError: (err: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(err.response?.data?.error || t("err.common.unknown", "오류가 발생했습니다."));
|
toast.error(
|
||||||
}
|
err.response?.data?.error ||
|
||||||
|
t("err.common.unknown", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const approveMutation = useMutation({
|
const approveMutation = useMutation({
|
||||||
@@ -83,18 +88,25 @@ export function TenantProfilePage() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenants"] });
|
queryClient.invalidateQueries({ queryKey: ["tenants"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||||
toast.success(t("msg.admin.tenants.approve_success", "테넌트가 승인되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.tenants.approve_success", "테넌트가 승인되었습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>) => {
|
onError: (err: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(err.response?.data?.error || t("err.common.unknown", "오류가 발생했습니다."));
|
toast.error(
|
||||||
}
|
err.response?.data?.error ||
|
||||||
|
t("err.common.unknown", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: () => deleteTenant(tenantId),
|
mutationFn: () => deleteTenant(tenantId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
navigate("/tenants");
|
navigate("/tenants");
|
||||||
toast.success(t("msg.admin.tenants.delete_success", "테넌트가 삭제되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.tenants.delete_success", "테넌트가 삭제되었습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,13 +116,23 @@ export function TenantProfilePage() {
|
|||||||
?.response?.data?.error;
|
?.response?.data?.error;
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (window.confirm(t("msg.admin.tenants.delete_confirm", "삭제하시겠습니까?", { name: tenantQuery.data?.name ?? "" }))) {
|
if (
|
||||||
|
window.confirm(
|
||||||
|
t("msg.admin.tenants.delete_confirm", "삭제하시겠습니까?", {
|
||||||
|
name: tenantQuery.data?.name ?? "",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
) {
|
||||||
deleteMutation.mutate();
|
deleteMutation.mutate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = () => {
|
const handleApprove = () => {
|
||||||
if (window.confirm(t("msg.admin.tenants.approve_confirm", "이 테넌트를 승인하시겠습니까?"))) {
|
if (
|
||||||
|
window.confirm(
|
||||||
|
t("msg.admin.tenants.approve_confirm", "이 테넌트를 승인하시겠습니까?"),
|
||||||
|
)
|
||||||
|
) {
|
||||||
approveMutation.mutate();
|
approveMutation.mutate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -119,9 +141,14 @@ export function TenantProfilePage() {
|
|||||||
<>
|
<>
|
||||||
<Card className="bg-[var(--color-panel)] mt-6">
|
<Card className="bg-[var(--color-panel)] mt-6">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("ui.admin.tenants.profile.title", "테넌트 프로필")}</CardTitle>
|
<CardTitle>
|
||||||
|
{t("ui.admin.tenants.profile.title", "테넌트 프로필")}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t("ui.admin.tenants.profile.subtitle", "슬러그 및 상태 변경은 즉시 적용됩니다.")}
|
{t(
|
||||||
|
"ui.admin.tenants.profile.subtitle",
|
||||||
|
"슬러그 및 상태 변경은 즉시 적용됩니다.",
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
@@ -132,30 +159,54 @@ export function TenantProfilePage() {
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-semibold">
|
<Label className="text-sm font-semibold">
|
||||||
{t("ui.admin.tenants.profile.name", "테넌트 이름")} <span className="text-destructive">*</span>
|
{t("ui.admin.tenants.profile.name", "테넌트 이름")}{" "}
|
||||||
|
<span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-semibold">{t("ui.admin.tenants.profile.type", "테넌트 유형")}</Label>
|
<Label className="text-sm font-semibold">
|
||||||
|
{t("ui.admin.tenants.profile.type", "테넌트 유형")}
|
||||||
|
</Label>
|
||||||
<select
|
<select
|
||||||
id="type"
|
id="type"
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
value={type}
|
value={type}
|
||||||
onChange={(e) => setType(e.target.value)}
|
onChange={(e) => setType(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="COMPANY">{t("domain.tenant_type.company", "COMPANY (일반 기업)")}</option>
|
<option value="COMPANY">
|
||||||
<option value="COMPANY_GROUP">{t("domain.tenant_type.company_group", "COMPANY_GROUP (그룹사/지주사)")}</option>
|
{t("domain.tenant_type.company", "COMPANY (일반 기업)")}
|
||||||
<option value="USER_GROUP">{t("domain.tenant_type.user_group", "USER_GROUP (내부 부서/팀)")}</option>
|
</option>
|
||||||
<option value="PERSONAL">{t("domain.tenant_type.personal", "PERSONAL (개인 워크스페이스)")}</option>
|
<option value="COMPANY_GROUP">
|
||||||
|
{t(
|
||||||
|
"domain.tenant_type.company_group",
|
||||||
|
"COMPANY_GROUP (그룹사/지주사)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
<option value="USER_GROUP">
|
||||||
|
{t(
|
||||||
|
"domain.tenant_type.user_group",
|
||||||
|
"USER_GROUP (내부 부서/팀)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
<option value="PERSONAL">
|
||||||
|
{t(
|
||||||
|
"domain.tenant_type.personal",
|
||||||
|
"PERSONAL (개인 워크스페이스)",
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-semibold">{t("ui.admin.tenants.profile.slug", "슬러그 (Slug)")}</Label>
|
<Label className="text-sm font-semibold">
|
||||||
|
{t("ui.admin.tenants.profile.slug", "슬러그 (Slug)")}
|
||||||
|
</Label>
|
||||||
<Input value={slug} onChange={(e) => setSlug(e.target.value)} />
|
<Input value={slug} onChange={(e) => setSlug(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-semibold">{t("ui.admin.tenants.profile.description", "설명")}</Label>
|
<Label className="text-sm font-semibold">
|
||||||
|
{t("ui.admin.tenants.profile.description", "설명")}
|
||||||
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={description}
|
value={description}
|
||||||
@@ -164,7 +215,10 @@ export function TenantProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-semibold">
|
<Label className="text-sm font-semibold">
|
||||||
{t("ui.admin.tenants.profile.allowed_domains", "허용된 도메인 (콤마로 구분)")}
|
{t(
|
||||||
|
"ui.admin.tenants.profile.allowed_domains",
|
||||||
|
"허용된 도메인 (콤마로 구분)",
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
value={domains}
|
value={domains}
|
||||||
@@ -172,11 +226,16 @@ export function TenantProfilePage() {
|
|||||||
placeholder="example.com, example.kr"
|
placeholder="example.com, example.kr"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("ui.admin.tenants.profile.allowed_domains_help", "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.")}
|
{t(
|
||||||
|
"ui.admin.tenants.profile.allowed_domains_help",
|
||||||
|
"이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-semibold">{t("ui.admin.tenants.profile.status", "상태")}</Label>
|
<Label className="text-sm font-semibold">
|
||||||
|
{t("ui.admin.tenants.profile.status", "상태")}
|
||||||
|
</Label>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -81,10 +81,18 @@ export function TenantSchemaPage() {
|
|||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||||
toast.success(t("msg.admin.tenants.schema.update_success", "스키마가 저장되었습니다."));
|
toast.success(
|
||||||
|
t(
|
||||||
|
"msg.admin.tenants.schema.update_success",
|
||||||
|
"스키마가 저장되었습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>) => {
|
onError: (err: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(err.response?.data?.error || t("msg.admin.tenants.schema.update_error", "저장에 실패했습니다."));
|
toast.error(
|
||||||
|
err.response?.data?.error ||
|
||||||
|
t("msg.admin.tenants.schema.update_error", "저장에 실패했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,13 +204,22 @@ export function TenantSchemaPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="text">
|
<option value="text">
|
||||||
{t("ui.admin.tenants.schema.field.type_text", "텍스트 (Text)")}
|
{t(
|
||||||
|
"ui.admin.tenants.schema.field.type_text",
|
||||||
|
"텍스트 (Text)",
|
||||||
|
)}
|
||||||
</option>
|
</option>
|
||||||
<option value="number">
|
<option value="number">
|
||||||
{t("ui.admin.tenants.schema.field.type_number", "숫자 (Number)")}
|
{t(
|
||||||
|
"ui.admin.tenants.schema.field.type_number",
|
||||||
|
"숫자 (Number)",
|
||||||
|
)}
|
||||||
</option>
|
</option>
|
||||||
<option value="boolean">
|
<option value="boolean">
|
||||||
{t("ui.admin.tenants.schema.field.type_boolean", "불리언 (Boolean)")}
|
{t(
|
||||||
|
"ui.admin.tenants.schema.field.type_boolean",
|
||||||
|
"불리언 (Boolean)",
|
||||||
|
)}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,31 +46,31 @@ import { t } from "../../../lib/i18n";
|
|||||||
type UserGroupNode = GroupSummary & { children: UserGroupNode[] };
|
type UserGroupNode = GroupSummary & { children: UserGroupNode[] };
|
||||||
|
|
||||||
function buildGroupTree(groups: GroupSummary[]): UserGroupNode[] {
|
function buildGroupTree(groups: GroupSummary[]): UserGroupNode[] {
|
||||||
const nodeMap = new Map<string, UserGroupNode>();
|
const nodeMap = new Map<string, UserGroupNode>();
|
||||||
const rootNodes: UserGroupNode[] = [];
|
const rootNodes: UserGroupNode[] = [];
|
||||||
|
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
nodeMap.set(group.id, { ...group, children: [] });
|
nodeMap.set(group.id, { ...group, children: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
const node = nodeMap.get(group.id)!;
|
const node = nodeMap.get(group.id)!;
|
||||||
if (group.parentId && nodeMap.has(group.parentId)) {
|
if (group.parentId && nodeMap.has(group.parentId)) {
|
||||||
const parent = nodeMap.get(group.parentId)!;
|
const parent = nodeMap.get(group.parentId)!;
|
||||||
parent.children.push(node);
|
parent.children.push(node);
|
||||||
} else {
|
} else {
|
||||||
rootNodes.push(node);
|
rootNodes.push(node);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortNodes = (nodes: UserGroupNode[]) => {
|
const sortNodes = (nodes: UserGroupNode[]) => {
|
||||||
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
nodes.forEach(node => sortNodes(node.children));
|
nodes.forEach((node) => sortNodes(node.children));
|
||||||
};
|
};
|
||||||
sortNodes(rootNodes);
|
sortNodes(rootNodes);
|
||||||
|
|
||||||
return rootNodes;
|
return rootNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserGroupTreeNodeProps {
|
interface UserGroupTreeNodeProps {
|
||||||
node: UserGroupNode;
|
node: UserGroupNode;
|
||||||
@@ -113,7 +113,11 @@ const UserGroupTreeNode: React.FC<UserGroupTreeNodeProps> = ({
|
|||||||
setIsExpanded(!isExpanded);
|
setIsExpanded(!isExpanded);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
{isExpanded ? (
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!hasChildren && <div className="w-6" />}
|
{!hasChildren && <div className="w-6" />}
|
||||||
@@ -125,7 +129,7 @@ const UserGroupTreeNode: React.FC<UserGroupTreeNodeProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge variant="secondary">{node.members?.length || 0}</Badge>
|
<Badge variant="secondary">{node.members?.length || 0}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button
|
<Button
|
||||||
@@ -151,237 +155,376 @@ const UserGroupTreeNode: React.FC<UserGroupTreeNodeProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{isExpanded && hasChildren && node.children.map((child) => (
|
{isExpanded &&
|
||||||
<UserGroupTreeNode
|
hasChildren &&
|
||||||
key={child.id}
|
node.children.map((child) => (
|
||||||
node={child}
|
<UserGroupTreeNode
|
||||||
level={level + 1}
|
key={child.id}
|
||||||
onSelect={onSelect}
|
node={child}
|
||||||
selectedGroupId={selectedGroupId}
|
level={level + 1}
|
||||||
onDelete={onDelete}
|
onSelect={onSelect}
|
||||||
onAddSubGroup={onAddSubGroup}
|
selectedGroupId={selectedGroupId}
|
||||||
/>
|
onDelete={onDelete}
|
||||||
))}
|
onAddSubGroup={onAddSubGroup}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export function TenantUserGroupsTab() {
|
export function TenantUserGroupsTab() {
|
||||||
const params = useParams<{ tenantId: string }>();
|
const params = useParams<{ tenantId: string }>();
|
||||||
const tenantId = params.tenantId ?? "";
|
const tenantId = params.tenantId ?? "";
|
||||||
|
|
||||||
const [newGroupName, setNewGroupName] = useState("");
|
const [newGroupName, setNewGroupName] = useState("");
|
||||||
const [newGroupDesc, setNewGroupNameDesc] = useState("");
|
const [newGroupDesc, setNewGroupNameDesc] = useState("");
|
||||||
const [newGroupUnitType, setNewGroupUnitType] = useState("Team");
|
const [newGroupUnitType, setNewGroupUnitType] = useState("Team");
|
||||||
const [newGroupParentId, setNewGroupParentId] = useState<string | null>(null);
|
const [newGroupParentId, setNewGroupParentId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
||||||
|
|
||||||
const groupsQuery = useQuery({
|
const groupsQuery = useQuery({
|
||||||
queryKey: ["groups", tenantId],
|
queryKey: ["groups", tenantId],
|
||||||
queryFn: () => fetchGroups(tenantId),
|
queryFn: () => fetchGroups(tenantId),
|
||||||
enabled: !!tenantId,
|
enabled: !!tenantId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
createGroup(tenantId, {
|
createGroup(tenantId, {
|
||||||
name: newGroupName,
|
name: newGroupName,
|
||||||
description: newGroupDesc,
|
description: newGroupDesc,
|
||||||
unitType: newGroupUnitType,
|
unitType: newGroupUnitType,
|
||||||
parentId: newGroupParentId || undefined,
|
parentId: newGroupParentId || undefined,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("msg.admin.groups.list.create_success", "그룹이 성공적으로 생성되었습니다."));
|
toast.success(
|
||||||
groupsQuery.refetch();
|
t(
|
||||||
setNewGroupName("");
|
"msg.admin.groups.list.create_success",
|
||||||
setNewGroupNameDesc("");
|
"그룹이 성공적으로 생성되었습니다.",
|
||||||
setNewGroupUnitType("Team");
|
),
|
||||||
setNewGroupParentId(null);
|
);
|
||||||
},
|
groupsQuery.refetch();
|
||||||
onError: (error: AxiosError<{ error?: string }>) => {
|
setNewGroupName("");
|
||||||
toast.error(t("msg.admin.groups.list.create_error", "그룹 생성 실패"), { description: error.response?.data?.error || error.message });
|
setNewGroupNameDesc("");
|
||||||
}
|
setNewGroupUnitType("Team");
|
||||||
});
|
setNewGroupParentId(null);
|
||||||
|
},
|
||||||
const deleteMutation = useMutation({
|
onError: (error: AxiosError<{ error?: string }>) => {
|
||||||
mutationFn: (id: string) => deleteGroup(tenantId, id),
|
toast.error(t("msg.admin.groups.list.create_error", "그룹 생성 실패"), {
|
||||||
onSuccess: () => {
|
description: error.response?.data?.error || error.message,
|
||||||
toast.success(t("msg.admin.groups.list.delete_success", "그룹이 삭제되었습니다."));
|
|
||||||
groupsQuery.refetch();
|
|
||||||
if (selectedGroupId && selectedGroupId === (deleteMutation.variables as any)) {
|
|
||||||
setSelectedGroupId(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error: AxiosError<{ error?: string }>) => {
|
|
||||||
toast.error(t("msg.common.error", "그룹 삭제 실패"), { description: error.response?.data?.error || error.message });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const addMemberMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
|
mutationFn: (id: string) => deleteGroup(tenantId, id),
|
||||||
addGroupMember(tenantId, groupId, userId),
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
toast.success(
|
||||||
toast.success(t("msg.admin.groups.members.add_success", "멤버가 추가되었습니다."));
|
t("msg.admin.groups.list.delete_success", "그룹이 삭제되었습니다."),
|
||||||
groupsQuery.refetch();
|
);
|
||||||
},
|
groupsQuery.refetch();
|
||||||
onError: (error: AxiosError<{ error?: string }>) => {
|
if (
|
||||||
toast.error(t("msg.common.error", "오류 발생"), { description: error.response?.data?.error || error.message });
|
selectedGroupId &&
|
||||||
|
selectedGroupId === (deleteMutation.variables as any)
|
||||||
|
) {
|
||||||
|
setSelectedGroupId(null);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
onError: (error: AxiosError<{ error?: string }>) => {
|
||||||
|
toast.error(t("msg.common.error", "그룹 삭제 실패"), {
|
||||||
|
description: error.response?.data?.error || error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const removeMemberMutation = useMutation({
|
const addMemberMutation = useMutation({
|
||||||
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
|
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
|
||||||
removeGroupMember(tenantId, groupId, userId),
|
addGroupMember(tenantId, groupId, userId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("msg.admin.groups.members.remove_success", "멤버가 제거되었습니다."));
|
toast.success(
|
||||||
groupsQuery.refetch();
|
t("msg.admin.groups.members.add_success", "멤버가 추가되었습니다."),
|
||||||
},
|
);
|
||||||
onError: (error: AxiosError<{ error?: string }>) => {
|
groupsQuery.refetch();
|
||||||
toast.error(t("msg.common.error", "오류 발생"), { description: error.response?.data?.error || error.message });
|
},
|
||||||
}
|
onError: (error: AxiosError<{ error?: string }>) => {
|
||||||
});
|
toast.error(t("msg.common.error", "오류 발생"), {
|
||||||
|
description: error.response?.data?.error || error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const groupTree = groupsQuery.data ? buildGroupTree(groupsQuery.data) : [];
|
const removeMemberMutation = useMutation({
|
||||||
|
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
|
||||||
|
removeGroupMember(tenantId, groupId, userId),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(
|
||||||
|
t("msg.admin.groups.members.remove_success", "멤버가 제거되었습니다."),
|
||||||
|
);
|
||||||
|
groupsQuery.refetch();
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError<{ error?: string }>) => {
|
||||||
|
toast.error(t("msg.common.error", "오류 발생"), {
|
||||||
|
description: error.response?.data?.error || error.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleAddSubGroup = (parentId: string) => {
|
const groupTree = groupsQuery.data ? buildGroupTree(groupsQuery.data) : [];
|
||||||
setNewGroupParentId(parentId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteGroup = (groupId: string, groupName: string) => {
|
const handleAddSubGroup = (parentId: string) => {
|
||||||
if (window.confirm(t("msg.admin.groups.list.delete_confirm", `그룹 "{{name}}"을(를) 삭제하시겠습니까?`, { name: groupName }))) {
|
setNewGroupParentId(parentId);
|
||||||
deleteMutation.mutate(groupId);
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddMember = (groupId: string) => {
|
const handleDeleteGroup = (groupId: string, groupName: string) => {
|
||||||
const userId = window.prompt(t("msg.admin.groups.prompt.user_id", "추가할 사용자의 UUID를 입력하세요:"));
|
if (
|
||||||
if (userId) {
|
window.confirm(
|
||||||
addMemberMutation.mutate({ groupId, userId });
|
t(
|
||||||
}
|
"msg.admin.groups.list.delete_confirm",
|
||||||
};
|
`그룹 "{{name}}"을(를) 삭제하시겠습니까?`,
|
||||||
|
{ name: groupName },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deleteMutation.mutate(groupId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const currentGroup = groupsQuery.data?.find((g) => g.id === selectedGroupId);
|
const handleAddMember = (groupId: string) => {
|
||||||
|
const userId = window.prompt(
|
||||||
|
t(
|
||||||
|
"msg.admin.groups.prompt.user_id",
|
||||||
|
"추가할 사용자의 UUID를 입력하세요:",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (userId) {
|
||||||
|
addMemberMutation.mutate({ groupId, userId });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const currentGroup = groupsQuery.data?.find((g) => g.id === selectedGroupId);
|
||||||
<div className="space-y-6 mt-6">
|
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
return (
|
||||||
<Card className="bg-[var(--color-panel)] md:col-span-1 border-primary/20">
|
<div className="space-y-6 mt-6">
|
||||||
<CardHeader>
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
<CardTitle className="text-lg flex items-center gap-2">
|
<Card className="bg-[var(--color-panel)] md:col-span-1 border-primary/20">
|
||||||
<Plus size={18} /> {t("ui.admin.groups.create.title", "새 그룹 생성")}
|
<CardHeader>
|
||||||
</CardTitle>
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
</CardHeader>
|
<Plus size={18} />{" "}
|
||||||
<CardContent className="space-y-4">
|
{t("ui.admin.groups.create.title", "새 그룹 생성")}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="name">{t("ui.admin.groups.form.name_label", "그룹 이름")}</Label>
|
<Label htmlFor="name">
|
||||||
<Input id="name" value={newGroupName} onChange={(e) => setNewGroupName(e.target.value)} />
|
{t("ui.admin.groups.form.name_label", "그룹 이름")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={newGroupName}
|
||||||
|
onChange={(e) => setNewGroupName(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="unitType">{t("ui.admin.groups.form.unit_level_label", "조직 단위 레벨")}</Label>
|
<Label htmlFor="unitType">
|
||||||
<Input id="unitType" value={newGroupUnitType} onChange={(e) => setNewGroupUnitType(e.target.value)} />
|
{t("ui.admin.groups.form.unit_level_label", "조직 단위 레벨")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="unitType"
|
||||||
|
value={newGroupUnitType}
|
||||||
|
onChange={(e) => setNewGroupUnitType(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="parentId">{t("ui.admin.groups.form.parent_label", "상위 그룹 (선택)")}</Label>
|
<Label htmlFor="parentId">
|
||||||
|
{t("ui.admin.groups.form.parent_label", "상위 그룹 (선택)")}
|
||||||
|
</Label>
|
||||||
<select
|
<select
|
||||||
id="parentId"
|
id="parentId"
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm"
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm"
|
||||||
value={newGroupParentId || ""}
|
value={newGroupParentId || ""}
|
||||||
onChange={(e) => setNewGroupParentId(e.target.value || null)}
|
onChange={(e) => setNewGroupParentId(e.target.value || null)}
|
||||||
>
|
>
|
||||||
<option value="">{t("ui.admin.groups.form.parent_none", "없음 (최상위)")}</option>
|
<option value="">
|
||||||
{groupsQuery.data?.map(group => <option key={group.id} value={group.id}>{group.name}</option>)}
|
{t("ui.admin.groups.form.parent_none", "없음 (최상위)")}
|
||||||
|
</option>
|
||||||
|
{groupsQuery.data?.map((group) => (
|
||||||
|
<option key={group.id} value={group.id}>
|
||||||
|
{group.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="desc">{t("ui.admin.groups.form.desc_label", "설명")}</Label>
|
<Label htmlFor="desc">
|
||||||
<Input id="desc" value={newGroupDesc} onChange={(e) => setNewGroupNameDesc(e.target.value)} />
|
{t("ui.admin.groups.form.desc_label", "설명")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="desc"
|
||||||
|
value={newGroupDesc}
|
||||||
|
onChange={(e) => setNewGroupNameDesc(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button className="w-full" onClick={() => createMutation.mutate()} disabled={!newGroupName || createMutation.isPending}>
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => createMutation.mutate()}
|
||||||
|
disabled={!newGroupName || createMutation.isPending}
|
||||||
|
>
|
||||||
{t("ui.admin.groups.form.submit", "생성하기")}
|
{t("ui.admin.groups.form.submit", "생성하기")}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-[var(--color-panel)] md:col-span-2">
|
<Card className="bg-[var(--color-panel)] md:col-span-2">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>{t("ui.admin.groups.list.title", "User Groups")}</CardTitle>
|
<CardTitle>
|
||||||
<CardDescription>{t("msg.admin.groups.list.subtitle", "이 테넌트에 정의된 사용자 그룹 목록입니다.")}</CardDescription>
|
{t("ui.admin.groups.list.title", "User Groups")}
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => groupsQuery.refetch()}><RefreshCw size={14} /></Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>{t("ui.admin.groups.table.name", "NAME")}</TableHead>
|
|
||||||
<TableHead className="text-center">{t("ui.admin.groups.table.members", "MEMBERS")}</TableHead>
|
|
||||||
<TableHead className="text-right">{t("ui.admin.groups.table.actions", "ACTIONS")}</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{groupsQuery.isLoading && <TableRow><TableCell colSpan={3}>{t("msg.admin.groups.list.loading", "로딩 중...")}</TableCell></TableRow>}
|
|
||||||
{!groupsQuery.isLoading && groupTree.length === 0 && <TableRow><TableCell colSpan={3} className="text-center py-8 text-muted-foreground">{t("msg.admin.groups.list.empty", "아직 등록된 그룹이 없습니다.")}</TableCell></TableRow>}
|
|
||||||
{groupTree.map(node => (
|
|
||||||
<UserGroupTreeNode
|
|
||||||
key={node.id}
|
|
||||||
node={node}
|
|
||||||
level={0}
|
|
||||||
onSelect={setSelectedGroupId}
|
|
||||||
selectedGroupId={selectedGroupId}
|
|
||||||
onDelete={handleDeleteGroup}
|
|
||||||
onAddSubGroup={handleAddSubGroup}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentGroup && (
|
|
||||||
<Card className="bg-[var(--color-panel)] border-t-4 border-t-primary">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Shield size={18} className="text-primary" /> {t("msg.admin.groups.members.title", "[{{name}}] 멤버 관리", { name: currentGroup.name })}
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
<CardDescription>
|
||||||
<CardContent>
|
{t(
|
||||||
<div className="flex justify-end mb-4">
|
"msg.admin.groups.list.subtitle",
|
||||||
<Button size="sm" onClick={() => handleAddMember(currentGroup.id)} disabled={addMemberMutation.isPending}>
|
"이 테넌트에 정의된 사용자 그룹 목록입니다.",
|
||||||
<UserPlus size={14} className="mr-1" /> {t("ui.common.add", "멤버 추가")}
|
)}
|
||||||
</Button>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<Button
|
||||||
<TableHeader>
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => groupsQuery.refetch()}
|
||||||
|
>
|
||||||
|
<RefreshCw size={14} />
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
{t("ui.admin.groups.table.name", "NAME")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center">
|
||||||
|
{t("ui.admin.groups.table.members", "MEMBERS")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">
|
||||||
|
{t("ui.admin.groups.table.actions", "ACTIONS")}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{groupsQuery.isLoading && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>{t("ui.admin.groups.members.table.name", "이름")}</TableHead>
|
<TableCell colSpan={3}>
|
||||||
<TableHead>{t("ui.admin.groups.members.table.email", "이메일")}</TableHead>
|
{t("msg.admin.groups.list.loading", "로딩 중...")}
|
||||||
<TableHead className="text-right">{t("ui.admin.groups.members.table.remove", "제거")}</TableHead>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
)}
|
||||||
<TableBody>
|
{!groupsQuery.isLoading && groupTree.length === 0 && (
|
||||||
{currentGroup.members?.length === 0 && <TableRow><TableCell colSpan={3} className="text-center py-4 text-muted-foreground">{t("msg.admin.groups.members.empty", "멤버가 없습니다.")}</TableCell></TableRow>}
|
<TableRow>
|
||||||
{currentGroup.members?.map(user => (
|
<TableCell
|
||||||
<TableRow key={user.id}>
|
colSpan={3}
|
||||||
<TableCell className="font-medium">{user.name}</TableCell>
|
className="text-center py-8 text-muted-foreground"
|
||||||
<TableCell className="text-muted-foreground">{user.email}</TableCell>
|
>
|
||||||
<TableCell className="text-right">
|
{t(
|
||||||
<Button variant="ghost" size="sm" onClick={() => removeMemberMutation.mutate({ groupId: currentGroup.id, userId: user.id })} disabled={removeMemberMutation.isPending}>
|
"msg.admin.groups.list.empty",
|
||||||
<UserMinus size={14} className="text-destructive" />
|
"아직 등록된 그룹이 없습니다.",
|
||||||
</Button>
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)}
|
||||||
</TableBody>
|
{groupTree.map((node) => (
|
||||||
</Table>
|
<UserGroupTreeNode
|
||||||
</CardContent>
|
key={node.id}
|
||||||
</Card>
|
node={node}
|
||||||
)}
|
level={0}
|
||||||
|
onSelect={setSelectedGroupId}
|
||||||
|
selectedGroupId={selectedGroupId}
|
||||||
|
onDelete={handleDeleteGroup}
|
||||||
|
onAddSubGroup={handleAddSubGroup}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TenantUserGroupsTab;
|
{currentGroup && (
|
||||||
|
<Card className="bg-[var(--color-panel)] border-t-4 border-t-primary">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Shield size={18} className="text-primary" />{" "}
|
||||||
|
{t("msg.admin.groups.members.title", "[{{name}}] 멤버 관리", {
|
||||||
|
name: currentGroup.name,
|
||||||
|
})}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex justify-end mb-4">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleAddMember(currentGroup.id)}
|
||||||
|
disabled={addMemberMutation.isPending}
|
||||||
|
>
|
||||||
|
<UserPlus size={14} className="mr-1" />{" "}
|
||||||
|
{t("ui.common.add", "멤버 추가")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
{t("ui.admin.groups.members.table.name", "이름")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
{t("ui.admin.groups.members.table.email", "이메일")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">
|
||||||
|
{t("ui.admin.groups.members.table.remove", "제거")}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{currentGroup.members?.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={3}
|
||||||
|
className="text-center py-4 text-muted-foreground"
|
||||||
|
>
|
||||||
|
{t("msg.admin.groups.members.empty", "멤버가 없습니다.")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{currentGroup.members?.map((user) => (
|
||||||
|
<TableRow key={user.id}>
|
||||||
|
<TableCell className="font-medium">{user.name}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">
|
||||||
|
{user.email}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
removeMemberMutation.mutate({
|
||||||
|
groupId: currentGroup.id,
|
||||||
|
userId: user.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={removeMemberMutation.isPending}
|
||||||
|
>
|
||||||
|
<UserMinus size={14} className="text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TenantUserGroupsTab;
|
||||||
|
|||||||
@@ -101,10 +101,14 @@ export function UserGroupDetailPage() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["user-group-detail", id] });
|
queryClient.invalidateQueries({ queryKey: ["user-group-detail", id] });
|
||||||
setIsAddMemberOpen(false);
|
setIsAddMemberOpen(false);
|
||||||
setSelectedUserId("");
|
setSelectedUserId("");
|
||||||
toast.success(t("msg.admin.groups.members.add_success", "구성원이 추가되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.groups.members.add_success", "구성원이 추가되었습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast.error(error.message || t("err.common.unknown", "오류가 발생했습니다."));
|
toast.error(
|
||||||
|
error.message || t("err.common.unknown", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,7 +116,12 @@ export function UserGroupDetailPage() {
|
|||||||
mutationFn: (userId: string) => removeGroupMember(tenantId!, id!, userId),
|
mutationFn: (userId: string) => removeGroupMember(tenantId!, id!, userId),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["user-group-detail", id] });
|
queryClient.invalidateQueries({ queryKey: ["user-group-detail", id] });
|
||||||
toast.success(t("msg.admin.groups.members.remove_success", "구성원이 제외되었습니다."));
|
toast.success(
|
||||||
|
t(
|
||||||
|
"msg.admin.groups.members.remove_success",
|
||||||
|
"구성원이 제외되었습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,10 +131,14 @@ export function UserGroupDetailPage() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
|
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
|
||||||
setIsAddRoleOpen(false);
|
setIsAddRoleOpen(false);
|
||||||
toast.success(t("msg.admin.groups.roles.assign_success", "역할이 할당되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.groups.roles.assign_success", "역할이 할당되었습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast.error(error.message || t("err.common.unknown", "오류가 발생했습니다."));
|
toast.error(
|
||||||
|
error.message || t("err.common.unknown", "오류가 발생했습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,7 +147,9 @@ export function UserGroupDetailPage() {
|
|||||||
removeGroupRole(tenantId!, id!, role.targetTenantId, role.relation),
|
removeGroupRole(tenantId!, id!, role.targetTenantId, role.relation),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
|
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
|
||||||
toast.success(t("msg.admin.groups.roles.remove_success", "역할이 회수되었습니다."));
|
toast.success(
|
||||||
|
t("msg.admin.groups.roles.remove_success", "역할이 회수되었습니다."),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,7 +185,10 @@ export function UserGroupDetailPage() {
|
|||||||
to={`/tenants/${tenantId}/organization`}
|
to={`/tenants/${tenantId}/organization`}
|
||||||
className="text-primary hover:underline text-sm"
|
className="text-primary hover:underline text-sm"
|
||||||
>
|
>
|
||||||
{t("ui.admin.groups.detail.breadcrumb_org", "조직 관리 목록으로 돌아가기")}
|
{t(
|
||||||
|
"ui.admin.groups.detail.breadcrumb_org",
|
||||||
|
"조직 관리 목록으로 돌아가기",
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +214,9 @@ export function UserGroupDetailPage() {
|
|||||||
{t("ui.admin.groups.detail.breadcrumb_org", "조직 관리")}
|
{t("ui.admin.groups.detail.breadcrumb_org", "조직 관리")}
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span className="text-foreground">{t("ui.admin.groups.detail.breadcrumb_unit", "조직 단위")}</span>
|
<span className="text-foreground">
|
||||||
|
{t("ui.admin.groups.detail.breadcrumb_unit", "조직 단위")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-primary/10 rounded-lg">
|
<div className="p-2 bg-primary/10 rounded-lg">
|
||||||
@@ -210,12 +230,17 @@ export function UserGroupDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{currentGroup.description || t("msg.common.no_description", "설명이 없습니다.")}
|
{currentGroup.description ||
|
||||||
|
t("msg.common.no_description", "설명이 없습니다.")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Badge variant="outline" className="font-normal">{t("ui.admin.groups.detail.breadcrumb_unit", "조직 단위")}</Badge>
|
<Badge variant="outline" className="font-normal">
|
||||||
<Badge variant="muted" className="font-normal">ID: {id?.split("-")[0]}...</Badge>
|
{t("ui.admin.groups.detail.breadcrumb_unit", "조직 단위")}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="muted" className="font-normal">
|
||||||
|
ID: {id?.split("-")[0]}...
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -224,8 +249,15 @@ export function UserGroupDetailPage() {
|
|||||||
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
|
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>{t("ui.admin.groups.detail.members_title", "구성원 관리")}</CardTitle>
|
<CardTitle>
|
||||||
<CardDescription>{t("ui.admin.groups.detail.members_subtitle", "이 조직에 소속된 사용자를 관리합니다.")}</CardDescription>
|
{t("ui.admin.groups.detail.members_title", "구성원 관리")}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t(
|
||||||
|
"ui.admin.groups.detail.members_subtitle",
|
||||||
|
"이 조직에 소속된 사용자를 관리합니다.",
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={isAddMemberOpen} onOpenChange={setIsAddMemberOpen}>
|
<Dialog open={isAddMemberOpen} onOpenChange={setIsAddMemberOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -236,16 +268,24 @@ export function UserGroupDetailPage() {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("ui.admin.groups.detail.members_title", "구성원 추가")}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{t("ui.admin.groups.detail.members_title", "구성원 추가")}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("ui.admin.groups.detail.members_subtitle", "사용자를 검색하여 조직 구성원으로 추가합니다.")}
|
{t(
|
||||||
|
"ui.admin.groups.detail.members_subtitle",
|
||||||
|
"사용자를 검색하여 조직 구성원으로 추가합니다.",
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("ui.common.search", "사용자 검색")}</Label>
|
<Label>{t("ui.common.search", "사용자 검색")}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("ui.admin.users.list.search_placeholder", "이메일 또는 이름으로 검색...")}
|
placeholder={t(
|
||||||
|
"ui.admin.users.list.search_placeholder",
|
||||||
|
"이메일 또는 이름으로 검색...",
|
||||||
|
)}
|
||||||
value={searchUser}
|
value={searchUser}
|
||||||
onChange={(e) => setSearchUser(e.target.value)}
|
onChange={(e) => setSearchUser(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -257,7 +297,12 @@ export function UserGroupDetailPage() {
|
|||||||
onValueChange={setSelectedUserId}
|
onValueChange={setSelectedUserId}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t("ui.common.select_placeholder", "사용자를 선택하세요")} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"ui.common.select_placeholder",
|
||||||
|
"사용자를 선택하세요",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{userList?.items.map((user) => (
|
{userList?.items.map((user) => (
|
||||||
@@ -291,30 +336,43 @@ export function UserGroupDetailPage() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-muted/30">
|
<TableHeader className="bg-muted/30">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="font-bold">{t("ui.admin.users.list.table.name_email", "사용자")}</TableHead>
|
<TableHead className="font-bold">
|
||||||
<TableHead className="text-right font-bold">{t("ui.admin.groups.table.actions", "액션")}</TableHead>
|
{t("ui.admin.users.list.table.name_email", "사용자")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right font-bold">
|
||||||
|
{t("ui.admin.groups.table.actions", "액션")}
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{!currentGroup.members || currentGroup.members.length === 0 ? (
|
{!currentGroup.members ||
|
||||||
|
currentGroup.members.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={2}
|
colSpan={2}
|
||||||
className="text-center py-8 text-muted-foreground"
|
className="text-center py-8 text-muted-foreground"
|
||||||
>
|
>
|
||||||
{t("msg.admin.groups.members.empty", "구성원이 없습니다.")}
|
{t(
|
||||||
|
"msg.admin.groups.members.empty",
|
||||||
|
"구성원이 없습니다.",
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
currentGroup.members.map((member) => (
|
currentGroup.members.map((member) => (
|
||||||
<TableRow key={member.id} className="hover:bg-muted/30 transition-colors">
|
<TableRow
|
||||||
|
key={member.id}
|
||||||
|
className="hover:bg-muted/30 transition-colors"
|
||||||
|
>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xs">
|
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xs">
|
||||||
{member.name.charAt(0)}
|
{member.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-sm">{member.name}</p>
|
<p className="font-medium text-sm">
|
||||||
|
{member.name}
|
||||||
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{member.email}
|
{member.email}
|
||||||
</p>
|
</p>
|
||||||
@@ -327,7 +385,15 @@ export function UserGroupDetailPage() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="text-destructive hover:bg-destructive/10"
|
className="text-destructive hover:bg-destructive/10"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (confirm(t("msg.admin.groups.members.remove_confirm", "제거하시겠습니까?", { name: member.name }))) {
|
if (
|
||||||
|
confirm(
|
||||||
|
t(
|
||||||
|
"msg.admin.groups.members.remove_confirm",
|
||||||
|
"제거하시겠습니까?",
|
||||||
|
{ name: member.name },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
removeMemberMutation.mutate(member.id);
|
removeMemberMutation.mutate(member.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -348,9 +414,14 @@ export function UserGroupDetailPage() {
|
|||||||
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
|
<Card className="border-none shadow-sm bg-[var(--color-panel)]">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>{t("ui.admin.groups.detail.permissions_title", "권한 관리")}</CardTitle>
|
<CardTitle>
|
||||||
|
{t("ui.admin.groups.detail.permissions_title", "권한 관리")}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t("ui.admin.groups.detail.permissions_subtitle", "이 조직이 다른 테넌트에 가지는 역할을 정의합니다.")}
|
{t(
|
||||||
|
"ui.admin.groups.detail.permissions_subtitle",
|
||||||
|
"이 조직이 다른 테넌트에 가지는 역할을 정의합니다.",
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={isAddRoleOpen} onOpenChange={setIsAddRoleOpen}>
|
<Dialog open={isAddRoleOpen} onOpenChange={setIsAddRoleOpen}>
|
||||||
@@ -362,20 +433,35 @@ export function UserGroupDetailPage() {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("ui.admin.groups.detail.permissions_title", "테넌트 역할 할당")}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{t(
|
||||||
|
"ui.admin.groups.detail.permissions_title",
|
||||||
|
"테넌트 역할 할당",
|
||||||
|
)}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("msg.admin.groups.roles.description", "이 조직의 구성원들이 대상 테넌트에서 상속받을 역할을 선택하세요.")}
|
{t(
|
||||||
|
"msg.admin.groups.roles.description",
|
||||||
|
"이 조직의 구성원들이 대상 테넌트에서 상속받을 역할을 선택하세요.",
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("ui.admin.users.detail.form.tenant", "대상 테넌트")}</Label>
|
<Label>
|
||||||
|
{t("ui.admin.users.detail.form.tenant", "대상 테넌트")}
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedTargetTenantId}
|
value={selectedTargetTenantId}
|
||||||
onValueChange={setSelectedTargetTenantId}
|
onValueChange={setSelectedTargetTenantId}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t("ui.admin.tenants.list.select_placeholder", "테넌트를 선택하세요")} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"ui.admin.tenants.list.select_placeholder",
|
||||||
|
"테넌트를 선택하세요",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{tenantList?.items.map((t) => (
|
{tenantList?.items.map((t) => (
|
||||||
@@ -387,7 +473,9 @@ export function UserGroupDetailPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{t("ui.admin.users.detail.form.role", "역할 (Relation)")}</Label>
|
<Label>
|
||||||
|
{t("ui.admin.users.detail.form.role", "역할 (Relation)")}
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedRelation}
|
value={selectedRelation}
|
||||||
onValueChange={setSelectedRelation}
|
onValueChange={setSelectedRelation}
|
||||||
@@ -431,9 +519,15 @@ export function UserGroupDetailPage() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-muted/30">
|
<TableHeader className="bg-muted/30">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="font-bold">{t("ui.admin.users.detail.form.tenant", "대상 테넌트")}</TableHead>
|
<TableHead className="font-bold">
|
||||||
<TableHead className="font-bold">{t("ui.admin.users.detail.form.role", "역할")}</TableHead>
|
{t("ui.admin.users.detail.form.tenant", "대상 테넌트")}
|
||||||
<TableHead className="text-right font-bold">{t("ui.admin.groups.table.actions", "액션")}</TableHead>
|
</TableHead>
|
||||||
|
<TableHead className="font-bold">
|
||||||
|
{t("ui.admin.users.detail.form.role", "역할")}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right font-bold">
|
||||||
|
{t("ui.admin.groups.table.actions", "액션")}
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -449,19 +543,28 @@ export function UserGroupDetailPage() {
|
|||||||
colSpan={3}
|
colSpan={3}
|
||||||
className="text-center py-8 text-muted-foreground"
|
className="text-center py-8 text-muted-foreground"
|
||||||
>
|
>
|
||||||
{t("msg.admin.groups.roles.empty", "할당된 역할이 없습니다.")}
|
{t(
|
||||||
|
"msg.admin.groups.roles.empty",
|
||||||
|
"할당된 역할이 없습니다.",
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
groupRoles.map((role, idx) => (
|
groupRoles.map((role, idx) => (
|
||||||
<TableRow key={`${role.tenantId}-${role.relation}-${idx}`} className="hover:bg-muted/30 transition-colors">
|
<TableRow
|
||||||
|
key={`${role.tenantId}-${role.relation}-${idx}`}
|
||||||
|
className="hover:bg-muted/30 transition-colors"
|
||||||
|
>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="font-medium text-sm">
|
<div className="font-medium text-sm">
|
||||||
{role.tenantName || role.tenantId}
|
{role.tenantName || role.tenantId}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline" className="capitalize font-normal">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="capitalize font-normal"
|
||||||
|
>
|
||||||
{role.relation}
|
{role.relation}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -471,7 +574,11 @@ export function UserGroupDetailPage() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="text-destructive hover:bg-destructive/10"
|
className="text-destructive hover:bg-destructive/10"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (confirm(t("msg.admin.groups.roles.remove_confirm"))) {
|
if (
|
||||||
|
confirm(
|
||||||
|
t("msg.admin.groups.roles.remove_confirm"),
|
||||||
|
)
|
||||||
|
) {
|
||||||
removeRoleMutation.mutate({
|
removeRoleMutation.mutate({
|
||||||
targetTenantId: role.tenantId,
|
targetTenantId: role.tenantId,
|
||||||
relation: role.relation,
|
relation: role.relation,
|
||||||
|
|||||||
@@ -200,7 +200,10 @@ function UserListPage() {
|
|||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
{t("ui.admin.users.list.table.position_job", "POSITION / JOB")}
|
{t(
|
||||||
|
"ui.admin.users.list.table.position_job",
|
||||||
|
"POSITION / JOB",
|
||||||
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
{t("ui.admin.users.list.table.created", "CREATED")}
|
{t("ui.admin.users.list.table.created", "CREATED")}
|
||||||
@@ -277,7 +280,9 @@ function UserListPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col text-sm">
|
<div className="flex flex-col text-sm">
|
||||||
<span className="font-medium">{user.position || "-"}</span>
|
<span className="font-medium">
|
||||||
|
{user.position || "-"}
|
||||||
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{user.jobTitle || "-"}
|
{user.jobTitle || "-"}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -44,12 +44,8 @@ function LanguageSelector() {
|
|||||||
className="rounded-full border border-border bg-transparent px-3 py-2 text-sm text-muted-foreground transition hover:bg-muted/20"
|
className="rounded-full border border-border bg-transparent px-3 py-2 text-sm text-muted-foreground transition hover:bg-muted/20"
|
||||||
aria-label={t("ui.common.language", "언어")}
|
aria-label={t("ui.common.language", "언어")}
|
||||||
>
|
>
|
||||||
<option value="ko">
|
<option value="ko">{t("ui.common.language_ko", "한국어")}</option>
|
||||||
{t("ui.common.language_ko", "한국어")}
|
<option value="en">{t("ui.common.language_en", "English")}</option>
|
||||||
</option>
|
|
||||||
<option value="en">
|
|
||||||
{t("ui.common.language_en", "English")}
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ function LoginPage() {
|
|||||||
<Card className="border-primary/20 bg-card/50 backdrop-blur-xl shadow-2xl">
|
<Card className="border-primary/20 bg-card/50 backdrop-blur-xl shadow-2xl">
|
||||||
<CardHeader className="space-y-1">
|
<CardHeader className="space-y-1">
|
||||||
<CardTitle className="text-2xl flex items-center gap-2">
|
<CardTitle className="text-2xl flex items-center gap-2">
|
||||||
<LogIn size={20} className="text-primary" />
|
<LogIn size={20} className="text-primary" />
|
||||||
개발자 포털 로그인
|
개발자 포털 로그인
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Baron 통합 인증(SSO)을 통해 개발자 포털에 접속합니다.
|
Baron 통합 인증(SSO)을 통해 개발자 포털에 접속합니다.
|
||||||
@@ -44,9 +44,9 @@ function LoginPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-4 pb-8 space-y-3">
|
<CardContent className="pt-4 pb-8 space-y-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSSOLogin}
|
onClick={handleSSOLogin}
|
||||||
className="w-full h-14 text-lg font-semibold flex gap-3 shadow-lg"
|
className="w-full h-14 text-lg font-semibold flex gap-3 shadow-lg"
|
||||||
disabled={auth.isLoading}
|
disabled={auth.isLoading}
|
||||||
>
|
>
|
||||||
{auth.isLoading ? (
|
{auth.isLoading ? (
|
||||||
<>
|
<>
|
||||||
@@ -63,20 +63,22 @@ function LoginPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="mt-6 text-xs text-center text-muted-foreground leading-relaxed">
|
<p className="mt-6 text-xs text-center text-muted-foreground leading-relaxed">
|
||||||
개발자 포털 세션은 브라우저 정책에 따라 유지됩니다.<br />
|
개발자 포털 세션은 브라우저 정책에 따라 유지됩니다.
|
||||||
|
<br />
|
||||||
민감한 작업 시 재인증을 요구할 수 있습니다.
|
민감한 작업 시 재인증을 요구할 수 있습니다.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-center gap-4">
|
<div className="flex justify-center gap-4">
|
||||||
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
||||||
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
||||||
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
<div className="h-1 w-1 rounded-full bg-primary/30"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="px-8 text-center text-sm text-muted-foreground">
|
<p className="px-8 text-center text-sm text-muted-foreground">
|
||||||
인증 정보가 없거나 로그인이 되지 않는 경우<br />
|
인증 정보가 없거나 로그인이 되지 않는 경우
|
||||||
|
<br />
|
||||||
시스템 관리자에게 문의하세요.
|
시스템 관리자에게 문의하세요.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -208,11 +208,7 @@ function ClientsPage() {
|
|||||||
<div className="mt-1 flex items-baseline gap-2">
|
<div className="mt-1 flex items-baseline gap-2">
|
||||||
<span className="text-3xl font-bold">{item.value}</span>
|
<span className="text-3xl font-bold">{item.value}</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={item.tone === "up" ? "success" : "muted"}
|
||||||
item.tone === "up"
|
|
||||||
? "success"
|
|
||||||
: "muted"
|
|
||||||
}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2",
|
"px-2",
|
||||||
item.tone === "stable" && "bg-muted/40 text-foreground",
|
item.tone === "stable" && "bg-muted/40 text-foreground",
|
||||||
|
|||||||
@@ -34,16 +34,12 @@ class _LocaleGateState extends State<LocaleGate> {
|
|||||||
Future<void> _applyLocale() async {
|
Future<void> _applyLocale() async {
|
||||||
final normalized = normalizeLocaleCode(widget.localeCode);
|
final normalized = normalizeLocaleCode(widget.localeCode);
|
||||||
LocaleStorage.write(normalized);
|
LocaleStorage.write(normalized);
|
||||||
webWindow.setTitle(
|
webWindow.setTitle(tr('ui.userfront.app_title'));
|
||||||
tr('ui.userfront.app_title'),
|
|
||||||
);
|
|
||||||
if (context.locale.languageCode == normalized) {
|
if (context.locale.languageCode == normalized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await context.setLocale(Locale(normalized));
|
await context.setLocale(Locale(normalized));
|
||||||
webWindow.setTitle(
|
webWindow.setTitle(tr('ui.userfront.app_title'));
|
||||||
tr('ui.userfront.app_title'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -299,10 +299,7 @@ class AuthProxyService {
|
|||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(
|
throw Exception(
|
||||||
errorBody['error'] ??
|
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_fetch'),
|
||||||
tr(
|
|
||||||
'err.userfront.auth_proxy.consent_fetch',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -333,10 +330,7 @@ class AuthProxyService {
|
|||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(
|
throw Exception(
|
||||||
errorBody['error'] ??
|
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_accept'),
|
||||||
tr(
|
|
||||||
'err.userfront.auth_proxy.consent_accept',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -363,10 +357,7 @@ class AuthProxyService {
|
|||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(
|
throw Exception(
|
||||||
errorBody['error'] ??
|
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_reject'),
|
||||||
tr(
|
|
||||||
'err.userfront.auth_proxy.consent_reject',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -810,7 +801,7 @@ class AuthProxyService {
|
|||||||
body: jsonEncode({
|
body: jsonEncode({
|
||||||
'level': level,
|
'level': level,
|
||||||
'message': message,
|
'message': message,
|
||||||
if (data != null) 'data': data,
|
'data': ?data,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
_recordClientLogSuccess();
|
_recordClientLogSuccess();
|
||||||
@@ -934,7 +925,7 @@ class AuthProxyService {
|
|||||||
'name': name,
|
'name': name,
|
||||||
'phone': phone,
|
'phone': phone,
|
||||||
'affiliationType': affiliationType,
|
'affiliationType': affiliationType,
|
||||||
if (companyCode != null) 'companyCode': companyCode,
|
'companyCode': ?companyCode,
|
||||||
'department': department,
|
'department': department,
|
||||||
'termsAccepted': termsAccepted,
|
'termsAccepted': termsAccepted,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ class LanguageSelector extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final current = context.locale.languageCode;
|
final current = context.locale.languageCode;
|
||||||
final items = [
|
final items = [
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(value: 'ko', child: Text(tr('ui.common.language_ko'))),
|
||||||
value: 'ko',
|
|
||||||
child: Text(tr('ui.common.language_ko')),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'en',
|
value: 'en',
|
||||||
child: Text(tr('ui.common.language_en', fallback: 'English')),
|
child: Text(tr('ui.common.language_en', fallback: 'English')),
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
'msg.userfront.error.title_with_code',
|
'msg.userfront.error.title_with_code',
|
||||||
params: {'code': normalizedCode},
|
params: {'code': normalizedCode},
|
||||||
)
|
)
|
||||||
: tr(
|
: tr('msg.userfront.error.title_generic'));
|
||||||
'msg.userfront.error.title_generic',
|
|
||||||
));
|
|
||||||
final detail = isProd
|
final detail = isProd
|
||||||
? (isInternalWhitelisted
|
? (isInternalWhitelisted
|
||||||
? tr(
|
? tr(
|
||||||
@@ -51,23 +49,16 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
: (isOryBypass
|
: (isOryBypass
|
||||||
? tr(
|
? tr(
|
||||||
'msg.userfront.error.ory.$normalizedCode',
|
'msg.userfront.error.ory.$normalizedCode',
|
||||||
fallback:
|
fallback: (description?.isNotEmpty == true)
|
||||||
(description?.isNotEmpty == true)
|
? description
|
||||||
? description
|
: tr('msg.userfront.error.detail_request'),
|
||||||
: tr('msg.userfront.error.detail_request'),
|
|
||||||
)
|
)
|
||||||
: tr(
|
: tr('msg.userfront.error.detail_contact')))
|
||||||
'msg.userfront.error.detail_contact',
|
|
||||||
)))
|
|
||||||
: ((description?.isNotEmpty == true)
|
: ((description?.isNotEmpty == true)
|
||||||
? description!
|
? description!
|
||||||
: (hasCode
|
: (hasCode
|
||||||
? tr(
|
? tr('msg.userfront.error.detail_generic')
|
||||||
'msg.userfront.error.detail_generic',
|
: tr('msg.userfront.error.detail_request')));
|
||||||
)
|
|
||||||
: tr(
|
|
||||||
'msg.userfront.error.detail_request',
|
|
||||||
)));
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF7F8FA),
|
backgroundColor: const Color(0xFFF7F8FA),
|
||||||
@@ -104,10 +95,7 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.error.type', params: {'type': errorType}),
|
||||||
'msg.userfront.error.type',
|
|
||||||
params: {'type': errorType},
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: const Color(0xFF6B7280),
|
color: const Color(0xFF6B7280),
|
||||||
),
|
),
|
||||||
@@ -115,10 +103,7 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
if (errorId != null && errorId!.isNotEmpty) ...[
|
if (errorId != null && errorId!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.error.id', params: {'id': errorId!}),
|
||||||
'msg.userfront.error.id',
|
|
||||||
params: {'id': errorId!},
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: const Color(0xFF6B7280),
|
color: const Color(0xFF6B7280),
|
||||||
),
|
),
|
||||||
@@ -142,11 +127,7 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(tr('ui.userfront.error.go_login')),
|
||||||
tr(
|
|
||||||
'ui.userfront.error.go_login',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () => context.go('/'),
|
onPressed: () => context.go('/'),
|
||||||
@@ -161,9 +142,7 @@ class ErrorScreen extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(tr('ui.userfront.error.go_home')),
|
||||||
tr('ui.userfront.error.go_home'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
Future<void> _handlePasswordReset() async {
|
Future<void> _handlePasswordReset() async {
|
||||||
final input = _loginIdController.text.trim();
|
final input = _loginIdController.text.trim();
|
||||||
if (input.isEmpty) {
|
if (input.isEmpty) {
|
||||||
_showError(
|
_showError(tr('msg.userfront.forgot.input_required'));
|
||||||
tr(
|
|
||||||
'msg.userfront.forgot.input_required',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +48,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(tr('msg.userfront.forgot.sent')),
|
||||||
tr(
|
|
||||||
'msg.userfront.forgot.sent',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -65,10 +57,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showError(
|
_showError(
|
||||||
tr(
|
tr('msg.userfront.forgot.error', params: {'error': e.toString()}),
|
||||||
'msg.userfront.forgot.error',
|
|
||||||
params: {'error': e.toString()},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -133,9 +122,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr('msg.userfront.forgot.dry_send'),
|
||||||
'msg.userfront.forgot.dry_send',
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Color(0xFF8A6D3B),
|
color: Color(0xFF8A6D3B),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -148,9 +135,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.forgot.description'),
|
||||||
'msg.userfront.forgot.description',
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
@@ -158,9 +143,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: _loginIdController,
|
controller: _loginIdController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.forgot.input_label'),
|
||||||
'ui.userfront.forgot.input_label',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
),
|
),
|
||||||
@@ -181,9 +164,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(tr('ui.userfront.forgot.submit')),
|
||||||
tr('ui.userfront.forgot.submit'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -900,8 +900,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
_onLoginSuccess(jwt, provider: provider, redirectTo: redirectTo);
|
_onLoginSuccess(jwt, provider: provider, redirectTo: redirectTo);
|
||||||
} else if (redirectTo != null && redirectTo.isNotEmpty) {
|
} else if (redirectTo != null && redirectTo.isNotEmpty) {
|
||||||
webWindow.redirectTo(redirectTo);
|
webWindow.redirectTo(redirectTo);
|
||||||
} else {
|
} else {}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.toString().contains("User not registered")) {
|
if (e.toString().contains("User not registered")) {
|
||||||
_showUnregisteredDialog();
|
_showUnregisteredDialog();
|
||||||
@@ -1124,11 +1123,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLoginSuccess(String token, {String? provider, String? redirectTo}) async {
|
Future<void> _onLoginSuccess(
|
||||||
|
String token, {
|
||||||
|
String? provider,
|
||||||
|
String? redirectTo,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Priority 1] Immediate External Redirection
|
// [Priority 1] Immediate External Redirection
|
||||||
@@ -1160,14 +1162,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
if (nextRedirectTo != null && nextRedirectTo.isNotEmpty) {
|
if (nextRedirectTo != null && nextRedirectTo.isNotEmpty) {
|
||||||
webWindow.redirectTo(nextRedirectTo); // Removed await
|
webWindow.redirectTo(nextRedirectTo); // Removed await
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showError(
|
_showError(tr('msg.userfront.login.oidc_failed'));
|
||||||
tr(
|
|
||||||
'msg.userfront.login.oidc_failed',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1188,7 +1185,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
|
|
||||||
final uri = Uri.base;
|
final uri = Uri.base;
|
||||||
final redirectParam =
|
final redirectParam =
|
||||||
uri.queryParameters['redirect_uri'] ?? uri.queryParameters['redirect_url'];
|
uri.queryParameters['redirect_uri'] ??
|
||||||
|
uri.queryParameters['redirect_url'];
|
||||||
final hasRedirectParam =
|
final hasRedirectParam =
|
||||||
redirectParam != null && redirectParam.isNotEmpty;
|
redirectParam != null && redirectParam.isNotEmpty;
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.login_success.subtitle'),
|
||||||
'msg.userfront.login_success.subtitle',
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||||
),
|
),
|
||||||
@@ -40,11 +38,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
|||||||
context.push('/scan');
|
context.push('/scan');
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.camera_alt, size: 28),
|
icon: const Icon(Icons.camera_alt, size: 28),
|
||||||
label: Text(
|
label: Text(tr('ui.userfront.login_success.qr')),
|
||||||
tr(
|
|
||||||
'ui.userfront.login_success.qr',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(80), // 버튼 높이를 더 크게
|
minimumSize: const Size.fromHeight(80), // 버튼 높이를 더 크게
|
||||||
backgroundColor: Colors.blue.shade700,
|
backgroundColor: Colors.blue.shade700,
|
||||||
@@ -63,9 +57,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
|||||||
context.go('/');
|
context.go('/');
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr('ui.userfront.login_success.later'),
|
||||||
'ui.userfront.login_success.later',
|
|
||||||
),
|
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: const Center(
|
body: const Center(
|
||||||
child: Text('QR Scanner is temporarily disabled for WASM build stability.'),
|
child: Text(
|
||||||
|
'QR Scanner is temporarily disabled for WASM build stability.',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,11 +69,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
if (_formKey.currentState?.validate() != true) return;
|
if (_formKey.currentState?.validate() != true) return;
|
||||||
if ((_loginId == null || _loginId!.isEmpty) &&
|
if ((_loginId == null || _loginId!.isEmpty) &&
|
||||||
(_token == null || _token!.isEmpty)) {
|
(_token == null || _token!.isEmpty)) {
|
||||||
_showError(
|
_showError(tr('msg.userfront.reset.invalid_link'));
|
||||||
tr(
|
|
||||||
'msg.userfront.reset.invalid_link',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +85,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(tr('msg.userfront.reset.success')),
|
||||||
tr(
|
|
||||||
'msg.userfront.reset.success',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -123,9 +115,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
|
|
||||||
String _buildPolicyDescription() {
|
String _buildPolicyDescription() {
|
||||||
if (_isPolicyLoading) {
|
if (_isPolicyLoading) {
|
||||||
return tr(
|
return tr('msg.userfront.reset.policy_loading');
|
||||||
'msg.userfront.reset.policy_loading',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||||
@@ -149,22 +139,16 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (requiresLower) {
|
if (requiresLower) {
|
||||||
parts.add(
|
parts.add(tr('msg.userfront.reset.policy.lowercase'));
|
||||||
tr('msg.userfront.reset.policy.lowercase'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (requiresUpper) {
|
if (requiresUpper) {
|
||||||
parts.add(
|
parts.add(tr('msg.userfront.reset.policy.uppercase'));
|
||||||
tr('msg.userfront.reset.policy.uppercase'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (requiresNumber) {
|
if (requiresNumber) {
|
||||||
parts.add(tr('msg.userfront.reset.policy.number'));
|
parts.add(tr('msg.userfront.reset.policy.number'));
|
||||||
}
|
}
|
||||||
if (requiresSymbol) {
|
if (requiresSymbol) {
|
||||||
parts.add(
|
parts.add(tr('msg.userfront.reset.policy.symbol'));
|
||||||
tr('msg.userfront.reset.policy.symbol'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join(", ");
|
return parts.join(", ");
|
||||||
@@ -192,9 +176,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('ui.userfront.reset.subtitle'),
|
||||||
'ui.userfront.reset.subtitle',
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -212,9 +194,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: _isPasswordObscured,
|
obscureText: _isPasswordObscured,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.reset.new_password'),
|
||||||
'ui.userfront.reset.new_password',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
@@ -265,25 +245,17 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((_policy?['lowercase'] ?? true) && !hasLower) {
|
if ((_policy?['lowercase'] ?? true) && !hasLower) {
|
||||||
return tr(
|
return tr('msg.userfront.reset.error.lowercase');
|
||||||
'msg.userfront.reset.error.lowercase',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if ((_policy?['uppercase'] ?? false) && !hasUpper) {
|
if ((_policy?['uppercase'] ?? false) && !hasUpper) {
|
||||||
return tr(
|
return tr('msg.userfront.reset.error.uppercase');
|
||||||
'msg.userfront.reset.error.uppercase',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if ((_policy?['number'] ?? true) && !hasNumber) {
|
if ((_policy?['number'] ?? true) && !hasNumber) {
|
||||||
return tr(
|
return tr('msg.userfront.reset.error.number');
|
||||||
'msg.userfront.reset.error.number',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if ((_policy?['nonAlphanumeric'] ?? true) &&
|
if ((_policy?['nonAlphanumeric'] ?? true) &&
|
||||||
!hasSymbol) {
|
!hasSymbol) {
|
||||||
return tr(
|
return tr('msg.userfront.reset.error.symbol');
|
||||||
'msg.userfront.reset.error.symbol',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -293,9 +265,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
controller: _confirmPasswordController,
|
controller: _confirmPasswordController,
|
||||||
obscureText: _isConfirmPasswordObscured,
|
obscureText: _isConfirmPasswordObscured,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.reset.confirm_password'),
|
||||||
'ui.userfront.reset.confirm_password',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
@@ -314,9 +284,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value != _passwordController.text) {
|
if (value != _passwordController.text) {
|
||||||
return tr(
|
return tr('msg.userfront.reset.error.mismatch');
|
||||||
'msg.userfront.reset.error.mismatch',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -336,11 +304,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(tr('ui.userfront.reset.submit')),
|
||||||
tr(
|
|
||||||
'ui.userfront.reset.submit',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -364,9 +328,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.reset.invalid_body'),
|
||||||
'msg.userfront.reset.invalid_body',
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -164,11 +164,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
final email = _emailController.text.trim();
|
final email = _emailController.text.trim();
|
||||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||||
if (!emailRegex.hasMatch(email)) {
|
if (!emailRegex.hasMatch(email)) {
|
||||||
setState(
|
setState(() => _emailError = tr('msg.userfront.signup.email.invalid'));
|
||||||
() => _emailError = tr(
|
|
||||||
'msg.userfront.signup.email.invalid',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -179,9 +175,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
final available = await AuthProxyService.checkEmailAvailability(email);
|
final available = await AuthProxyService.checkEmailAvailability(email);
|
||||||
if (!available) {
|
if (!available) {
|
||||||
setState(
|
setState(
|
||||||
() => _emailError = tr(
|
() => _emailError = tr('msg.userfront.signup.email.duplicate'),
|
||||||
'msg.userfront.signup.email.duplicate',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -217,9 +211,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(
|
setState(
|
||||||
() => _emailError = tr(
|
() => _emailError = tr('msg.userfront.signup.email.code_mismatch'),
|
||||||
'msg.userfront.signup.email.code_mismatch',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -272,9 +264,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(
|
setState(
|
||||||
() => _phoneError = tr(
|
() => _phoneError = tr('msg.userfront.signup.phone.code_mismatch'),
|
||||||
'msg.userfront.signup.phone.code_mismatch',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -329,17 +319,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
'msg.userfront.signup.password.lowercase_required',
|
'msg.userfront.signup.password.lowercase_required',
|
||||||
);
|
);
|
||||||
} else if (eStr.contains('digit') || eStr.contains('number')) {
|
} else if (eStr.contains('digit') || eStr.contains('number')) {
|
||||||
_passwordError = tr(
|
_passwordError = tr('msg.userfront.signup.password.number_required');
|
||||||
'msg.userfront.signup.password.number_required',
|
|
||||||
);
|
|
||||||
} else if (eStr.contains('symbol') || eStr.contains('special')) {
|
} else if (eStr.contains('symbol') || eStr.contains('special')) {
|
||||||
_passwordError = tr(
|
_passwordError = tr('msg.userfront.signup.password.symbol_required');
|
||||||
'msg.userfront.signup.password.symbol_required',
|
|
||||||
);
|
|
||||||
} else if (eStr.contains('length') || eStr.contains('12 characters')) {
|
} else if (eStr.contains('length') || eStr.contains('12 characters')) {
|
||||||
_passwordError = tr(
|
_passwordError = tr('msg.userfront.signup.password.length_required');
|
||||||
'msg.userfront.signup.password.length_required',
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
_passwordError = tr(
|
_passwordError = tr(
|
||||||
'msg.userfront.signup.failed',
|
'msg.userfront.signup.failed',
|
||||||
@@ -357,18 +341,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(
|
title: Text(tr('msg.userfront.signup.success.title')),
|
||||||
tr('msg.userfront.signup.success.title'),
|
content: Text(tr('msg.userfront.signup.success.body')),
|
||||||
),
|
|
||||||
content: Text(
|
|
||||||
tr('msg.userfront.signup.success.body'),
|
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.go('/signin'),
|
onPressed: () => context.go('/signin'),
|
||||||
child: Text(
|
child: Text(tr('ui.userfront.signup.success.action')),
|
||||||
tr('ui.userfront.signup.success.action'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -382,25 +360,13 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_stepCircle(
|
_stepCircle(1, tr('ui.userfront.signup.steps.agreement')),
|
||||||
1,
|
|
||||||
tr('ui.userfront.signup.steps.agreement'),
|
|
||||||
),
|
|
||||||
_stepLine(1),
|
_stepLine(1),
|
||||||
_stepCircle(
|
_stepCircle(2, tr('ui.userfront.signup.steps.verify')),
|
||||||
2,
|
|
||||||
tr('ui.userfront.signup.steps.verify'),
|
|
||||||
),
|
|
||||||
_stepLine(2),
|
_stepLine(2),
|
||||||
_stepCircle(
|
_stepCircle(3, tr('ui.userfront.signup.steps.profile')),
|
||||||
3,
|
|
||||||
tr('ui.userfront.signup.steps.profile'),
|
|
||||||
),
|
|
||||||
_stepLine(3),
|
_stepLine(3),
|
||||||
_stepCircle(
|
_stepCircle(4, tr('ui.userfront.signup.steps.password')),
|
||||||
4,
|
|
||||||
tr('ui.userfront.signup.steps.password'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -454,9 +420,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.signup.agreement.title'),
|
||||||
'msg.userfront.signup.agreement.title',
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -489,18 +453,14 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_agreementSection(
|
_agreementSection(
|
||||||
title: tr(
|
title: tr('ui.userfront.signup.agreement.tos_title'),
|
||||||
'ui.userfront.signup.agreement.tos_title',
|
|
||||||
),
|
|
||||||
content: _tosText,
|
content: _tosText,
|
||||||
value: _termsAccepted,
|
value: _termsAccepted,
|
||||||
onChanged: (val) => setState(() => _termsAccepted = val!),
|
onChanged: (val) => setState(() => _termsAccepted = val!),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_agreementSection(
|
_agreementSection(
|
||||||
title: tr(
|
title: tr('ui.userfront.signup.agreement.privacy_title'),
|
||||||
'ui.userfront.signup.agreement.privacy_title',
|
|
||||||
),
|
|
||||||
content: _privacyText,
|
content: _privacyText,
|
||||||
value: _privacyAccepted,
|
value: _privacyAccepted,
|
||||||
onChanged: (val) => setState(() => _privacyAccepted = val!),
|
onChanged: (val) => setState(() => _privacyAccepted = val!),
|
||||||
@@ -745,9 +705,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.signup.auth.title'),
|
||||||
'msg.userfront.signup.auth.title',
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -764,9 +722,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr('msg.userfront.signup.auth.affiliate_notice'),
|
||||||
'msg.userfront.signup.auth.affiliate_notice',
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
@@ -790,9 +746,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
onChanged: _checkEmailAffiliation, // 도메인 실시간 체크
|
onChanged: _checkEmailAffiliation, // 도메인 실시간 체크
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.signup.auth.email.label'),
|
||||||
'ui.userfront.signup.auth.email.label',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _emailError,
|
errorText: _emailError,
|
||||||
hintText: 'example@hanmaceng.co.kr',
|
hintText: 'example@hanmaceng.co.kr',
|
||||||
@@ -815,9 +769,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
_emailSeconds > 0
|
_emailSeconds > 0
|
||||||
? tr('ui.common.resend')
|
? tr('ui.common.resend')
|
||||||
: tr(
|
: tr('ui.userfront.signup.auth.request_code'),
|
||||||
'ui.userfront.signup.auth.request_code',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -828,9 +780,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailCodeController,
|
controller: _emailCodeController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.signup.auth.code_label'),
|
||||||
'ui.userfront.signup.auth.code_label',
|
|
||||||
),
|
|
||||||
suffixText: _formatTime(_emailSeconds),
|
suffixText: _formatTime(_emailSeconds),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
@@ -848,9 +798,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr('msg.userfront.signup.email.verified'),
|
||||||
'msg.userfront.signup.email.verified',
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -870,9 +818,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _phoneController,
|
controller: _phoneController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.signup.phone.label'),
|
||||||
'ui.userfront.signup.phone.label',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _phoneError,
|
errorText: _phoneError,
|
||||||
),
|
),
|
||||||
@@ -895,9 +841,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
_phoneSeconds > 0
|
_phoneSeconds > 0
|
||||||
? tr('ui.common.resend')
|
? tr('ui.common.resend')
|
||||||
: tr(
|
: tr('ui.userfront.signup.auth.request_code'),
|
||||||
'ui.userfront.signup.auth.request_code',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -908,9 +852,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _phoneCodeController,
|
controller: _phoneCodeController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.signup.auth.code_label'),
|
||||||
'ui.userfront.signup.auth.code_label',
|
|
||||||
),
|
|
||||||
suffixText: _formatTime(_phoneSeconds),
|
suffixText: _formatTime(_phoneSeconds),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
@@ -928,9 +870,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr('msg.userfront.signup.phone.verified'),
|
||||||
'msg.userfront.signup.phone.verified',
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -947,9 +887,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.signup.profile.title'),
|
||||||
'msg.userfront.signup.profile.title',
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -971,28 +909,20 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
key: ValueKey(_affiliationType),
|
key: ValueKey(_affiliationType),
|
||||||
initialValue: _affiliationType,
|
initialValue: _affiliationType,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.signup.profile.affiliation_type'),
|
||||||
'ui.userfront.signup.profile.affiliation_type',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
helperText: _isAffiliateEmail
|
helperText: _isAffiliateEmail
|
||||||
? tr(
|
? tr('msg.userfront.signup.profile.affiliate_hint')
|
||||||
'msg.userfront.signup.profile.affiliate_hint',
|
|
||||||
)
|
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'GENERAL',
|
value: 'GENERAL',
|
||||||
child: Text(
|
child: Text(tr('domain.affiliation.general')),
|
||||||
tr('domain.affiliation.general'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
value: 'AFFILIATE',
|
value: 'AFFILIATE',
|
||||||
child: Text(
|
child: Text(tr('domain.affiliation.affiliate')),
|
||||||
tr('domain.affiliation.affiliate'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onChanged: _isAffiliateEmail
|
onChanged: _isAffiliateEmail
|
||||||
@@ -1019,9 +949,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
key: ValueKey(_companyCode ?? 'none'),
|
key: ValueKey(_companyCode ?? 'none'),
|
||||||
initialValue: _companyCode,
|
initialValue: _companyCode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.signup.profile.company'),
|
||||||
'ui.userfront.signup.profile.company',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
@@ -1064,9 +992,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: _affiliationType == 'AFFILIATE'
|
labelText: _affiliationType == 'AFFILIATE'
|
||||||
? tr('ui.userfront.signup.profile.department')
|
? tr('ui.userfront.signup.profile.department')
|
||||||
: tr(
|
: tr('ui.userfront.signup.profile.department_optional'),
|
||||||
'ui.userfront.signup.profile.department_optional',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1076,9 +1002,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
|
|
||||||
String _buildPolicyDescription() {
|
String _buildPolicyDescription() {
|
||||||
if (_isPolicyLoading) {
|
if (_isPolicyLoading) {
|
||||||
return tr(
|
return tr('msg.userfront.signup.policy.loading');
|
||||||
'msg.userfront.signup.policy.loading',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||||
@@ -1147,9 +1071,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.signup.password.title'),
|
||||||
'msg.userfront.signup.password.title',
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -1183,9 +1105,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
obscureText: true,
|
obscureText: true,
|
||||||
onChanged: (_) => setState(() {}),
|
onChanged: (_) => setState(() {}),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.signup.password.label'),
|
||||||
'ui.userfront.signup.password.label',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _passwordError,
|
errorText: _passwordError,
|
||||||
),
|
),
|
||||||
@@ -1211,16 +1131,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
),
|
),
|
||||||
if (requiresUpper)
|
if (requiresUpper)
|
||||||
_cryptoCheck(
|
_cryptoCheck(
|
||||||
tr(
|
tr('msg.userfront.signup.password.rule.uppercase'),
|
||||||
'msg.userfront.signup.password.rule.uppercase',
|
|
||||||
),
|
|
||||||
hasUpper,
|
hasUpper,
|
||||||
),
|
),
|
||||||
if (requiresLower)
|
if (requiresLower)
|
||||||
_cryptoCheck(
|
_cryptoCheck(
|
||||||
tr(
|
tr('msg.userfront.signup.password.rule.lowercase'),
|
||||||
'msg.userfront.signup.password.rule.lowercase',
|
|
||||||
),
|
|
||||||
hasLower,
|
hasLower,
|
||||||
),
|
),
|
||||||
if (requiresNumber)
|
if (requiresNumber)
|
||||||
@@ -1230,9 +1146,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
),
|
),
|
||||||
if (requiresSymbol)
|
if (requiresSymbol)
|
||||||
_cryptoCheck(
|
_cryptoCheck(
|
||||||
tr(
|
tr('msg.userfront.signup.password.rule.symbol'),
|
||||||
'msg.userfront.signup.password.rule.symbol',
|
|
||||||
),
|
|
||||||
hasSpecial,
|
hasSpecial,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1244,16 +1158,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_confirmPasswordError = (val != _passwordController.text)
|
_confirmPasswordError = (val != _passwordController.text)
|
||||||
? tr(
|
? tr('msg.userfront.signup.password.mismatch')
|
||||||
'msg.userfront.signup.password.mismatch',
|
|
||||||
)
|
|
||||||
: null;
|
: null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.signup.password.confirm_label'),
|
||||||
'ui.userfront.signup.password.confirm_label',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: _confirmPasswordError,
|
errorText: _confirmPasswordError,
|
||||||
),
|
),
|
||||||
@@ -1379,12 +1289,8 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
_currentStep < 4
|
_currentStep < 4
|
||||||
? tr(
|
? tr('ui.userfront.signup.next_step')
|
||||||
'ui.userfront.signup.next_step',
|
: tr('ui.userfront.signup.complete'),
|
||||||
)
|
|
||||||
: tr(
|
|
||||||
'ui.userfront.signup.complete',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -172,9 +172,7 @@ class AuthTimelineNotifier extends Notifier<AuthTimelineState> {
|
|||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isLoadingMore: false,
|
isLoadingMore: false,
|
||||||
error: tr(
|
error: tr('msg.userfront.dashboard.timeline.load_error'),
|
||||||
'msg.userfront.dashboard.timeline.load_error',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(
|
title: Text(tr('ui.userfront.dashboard.revoke.title')),
|
||||||
tr('ui.userfront.dashboard.revoke.title'),
|
|
||||||
),
|
|
||||||
content: Text(
|
content: Text(
|
||||||
tr(
|
tr(
|
||||||
'msg.userfront.dashboard.revoke.confirm',
|
'msg.userfront.dashboard.revoke.confirm',
|
||||||
@@ -88,11 +86,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
child: Text(
|
child: Text(tr('ui.userfront.dashboard.revoke.confirm_button')),
|
||||||
tr(
|
|
||||||
'ui.userfront.dashboard.revoke.confirm_button',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -166,17 +160,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('ui.userfront.dashboard.scopes.title'),
|
||||||
'ui.userfront.dashboard.scopes.title',
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (item.scopes.isEmpty)
|
if (item.scopes.isEmpty)
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.dashboard.scopes.empty'),
|
||||||
'msg.userfront.dashboard.scopes.empty',
|
|
||||||
),
|
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -199,9 +189,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('ui.userfront.dashboard.status_history'),
|
||||||
'ui.userfront.dashboard.status_history',
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -219,9 +207,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
final statusLabel = item.status == 'active'
|
final statusLabel = item.status == 'active'
|
||||||
? tr('ui.common.status.active')
|
? tr('ui.common.status.active')
|
||||||
: tr(
|
: tr('ui.userfront.dashboard.status.revoked');
|
||||||
'ui.userfront.dashboard.status.revoked',
|
|
||||||
);
|
|
||||||
return Text(
|
return Text(
|
||||||
tr(
|
tr(
|
||||||
'msg.userfront.dashboard.current_status',
|
'msg.userfront.dashboard.current_status',
|
||||||
@@ -534,12 +520,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
? log.detailMap['approved_session_id'].toString()
|
? log.detailMap['approved_session_id'].toString()
|
||||||
: log.sessionId;
|
: log.sessionId;
|
||||||
final tooltipLabel = isOidc
|
final tooltipLabel = isOidc
|
||||||
? tr(
|
? tr('ui.userfront.dashboard.approved_session.userfront')
|
||||||
'ui.userfront.dashboard.approved_session.userfront',
|
: tr('ui.userfront.dashboard.approved_session.default');
|
||||||
)
|
|
||||||
: tr(
|
|
||||||
'ui.userfront.dashboard.approved_session.default',
|
|
||||||
);
|
|
||||||
final tooltip = approvedSessionId.isEmpty
|
final tooltip = approvedSessionId.isEmpty
|
||||||
? tr(
|
? tr(
|
||||||
'msg.userfront.dashboard.approved_session.none',
|
'msg.userfront.dashboard.approved_session.none',
|
||||||
@@ -558,9 +540,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
tr(
|
tr('msg.userfront.dashboard.session_id_copied'),
|
||||||
'msg.userfront.dashboard.session_id_copied',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -628,12 +608,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
? log.detailMap['approved_session_id'].toString()
|
? log.detailMap['approved_session_id'].toString()
|
||||||
: log.sessionId;
|
: log.sessionId;
|
||||||
final tooltipLabel = isOidc
|
final tooltipLabel = isOidc
|
||||||
? tr(
|
? tr('ui.userfront.dashboard.approved_session.userfront')
|
||||||
'ui.userfront.dashboard.approved_session.userfront',
|
: tr('ui.userfront.dashboard.approved_session.default');
|
||||||
)
|
|
||||||
: tr(
|
|
||||||
'ui.userfront.dashboard.approved_session.default',
|
|
||||||
);
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: approvedSessionId.isEmpty
|
onTap: approvedSessionId.isEmpty
|
||||||
? null
|
? null
|
||||||
@@ -643,9 +619,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
tr(
|
tr('msg.userfront.dashboard.session_id_copied'),
|
||||||
'msg.userfront.dashboard.session_id_copied',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -692,9 +666,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
final label = _appLabelForLog(log);
|
final label = _appLabelForLog(log);
|
||||||
final clientId = log.clientId;
|
final clientId = log.clientId;
|
||||||
final tooltip = clientId.isEmpty
|
final tooltip = clientId.isEmpty
|
||||||
? tr(
|
? tr('msg.userfront.dashboard.client_id_missing')
|
||||||
'msg.userfront.dashboard.client_id_missing',
|
|
||||||
)
|
|
||||||
: tr(
|
: tr(
|
||||||
'msg.userfront.dashboard.client_id',
|
'msg.userfront.dashboard.client_id',
|
||||||
fallback: 'Client ID: {{id}}',
|
fallback: 'Client ID: {{id}}',
|
||||||
@@ -814,21 +786,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
],
|
],
|
||||||
_buildSectionTitle(
|
_buildSectionTitle(
|
||||||
tr(
|
tr('ui.userfront.sections.apps'),
|
||||||
'ui.userfront.sections.apps',
|
tr('msg.userfront.sections.apps_subtitle'),
|
||||||
),
|
|
||||||
tr(
|
|
||||||
'msg.userfront.sections.apps_subtitle',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildActivitySection(isMobile),
|
_buildActivitySection(isMobile),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_buildSectionTitle(
|
_buildSectionTitle(
|
||||||
tr('ui.userfront.sections.audit'),
|
tr('ui.userfront.sections.audit'),
|
||||||
tr(
|
tr('msg.userfront.sections.audit_subtitle'),
|
||||||
'msg.userfront.sections.audit_subtitle',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildAccessHistory(timelineState, timelineWide),
|
_buildAccessHistory(timelineState, timelineWide),
|
||||||
@@ -857,10 +823,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.greeting', params: {'name': userName}),
|
||||||
'msg.userfront.greeting',
|
|
||||||
params: {'name': userName},
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -963,9 +926,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.dashboard.activities.empty'),
|
||||||
'msg.userfront.dashboard.activities.empty',
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[700],
|
color: Colors.grey[700],
|
||||||
@@ -974,9 +935,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.dashboard.activities.empty_detail'),
|
||||||
'msg.userfront.dashboard.activities.empty_detail',
|
|
||||||
),
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -992,9 +951,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.dashboard.activities.error'),
|
||||||
'msg.userfront.dashboard.activities.error',
|
|
||||||
),
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -1194,9 +1151,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
item.status == 'active'
|
item.status == 'active'
|
||||||
? tr('ui.common.status.active')
|
? tr('ui.common.status.active')
|
||||||
: tr(
|
: tr('ui.userfront.dashboard.status.revoked'),
|
||||||
'ui.userfront.dashboard.status.revoked',
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
@@ -1264,12 +1219,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
item.isRevoked
|
item.isRevoked
|
||||||
? tr(
|
? tr('ui.userfront.dashboard.status.revoked')
|
||||||
'ui.userfront.dashboard.status.revoked',
|
: tr('ui.userfront.dashboard.revoke.title'),
|
||||||
)
|
|
||||||
: tr(
|
|
||||||
'ui.userfront.dashboard.revoke.title',
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1303,22 +1254,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
}
|
}
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(tr('msg.userfront.dashboard.link_open_error')),
|
||||||
tr(
|
|
||||||
'msg.userfront.dashboard.link_open_error',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(tr('msg.userfront.dashboard.link_missing')),
|
||||||
tr(
|
|
||||||
'msg.userfront.dashboard.link_missing',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1344,11 +1287,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(tr('msg.userfront.dashboard.audit_load_error')),
|
||||||
tr(
|
|
||||||
'msg.userfront.dashboard.audit_load_error',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
@@ -1365,9 +1304,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
return _buildHistoryContainer(
|
return _buildHistoryContainer(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr('msg.userfront.dashboard.audit_empty'),
|
||||||
'msg.userfront.dashboard.audit_empty',
|
|
||||||
),
|
|
||||||
style: TextStyle(color: Colors.grey[600]),
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1416,16 +1353,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: Text(
|
label: Text(tr('ui.userfront.audit.table.date')),
|
||||||
tr('ui.userfront.audit.table.date'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: Text(
|
label: Text(tr('ui.userfront.audit.table.app')),
|
||||||
tr(
|
|
||||||
'ui.userfront.audit.table.app',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: Text(
|
label: Text(
|
||||||
@@ -1433,30 +1364,16 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: Text(
|
label: Text(tr('ui.userfront.audit.table.device')),
|
||||||
tr(
|
|
||||||
'ui.userfront.audit.table.device',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: Text(
|
label: Text(tr('ui.userfront.audit.table.auth_method')),
|
||||||
tr(
|
|
||||||
'ui.userfront.audit.table.auth_method',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: Text(
|
label: Text(tr('ui.userfront.audit.table.result')),
|
||||||
tr(
|
|
||||||
'ui.userfront.audit.table.result',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: Text(
|
label: Text(tr('ui.userfront.audit.table.status')),
|
||||||
tr('ui.userfront.audit.table.status'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
rows: state.items.map((log) {
|
rows: state.items.map((log) {
|
||||||
@@ -1505,9 +1422,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
DataCell(
|
DataCell(
|
||||||
_selectableText(
|
_selectableText(
|
||||||
tr(
|
tr('ui.userfront.audit.table.pending'),
|
||||||
'ui.userfront.audit.table.pending',
|
|
||||||
),
|
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1643,11 +1558,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(tr('msg.userfront.audit.load_more_error')),
|
||||||
tr(
|
|
||||||
'msg.userfront.audit.load_more_error',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
ref.read(authTimelineProvider.notifier).loadMore(),
|
ref.read(authTimelineProvider.notifier).loadMore(),
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ class ProfileRepository {
|
|||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) {
|
if (token == null && !useCookie) {
|
||||||
throw Exception(
|
throw Exception(tr('err.userfront.session.missing'));
|
||||||
tr('err.userfront.session.missing'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||||
@@ -59,9 +57,7 @@ class ProfileRepository {
|
|||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) {
|
if (token == null && !useCookie) {
|
||||||
throw Exception(
|
throw Exception(tr('err.userfront.session.missing'));
|
||||||
tr('err.userfront.session.missing'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||||
@@ -95,9 +91,7 @@ class ProfileRepository {
|
|||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) {
|
if (token == null && !useCookie) {
|
||||||
throw Exception(
|
throw Exception(tr('err.userfront.session.missing'));
|
||||||
tr('err.userfront.session.missing'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/send-code');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me/send-code');
|
||||||
@@ -130,9 +124,7 @@ class ProfileRepository {
|
|||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) {
|
if (token == null && !useCookie) {
|
||||||
throw Exception(
|
throw Exception(tr('err.userfront.session.missing'));
|
||||||
tr('err.userfront.session.missing'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/password');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me/password');
|
||||||
@@ -165,9 +157,7 @@ class ProfileRepository {
|
|||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
final useCookie = AuthTokenStore.usesCookie();
|
final useCookie = AuthTokenStore.usesCookie();
|
||||||
if (token == null && !useCookie) {
|
if (token == null && !useCookie) {
|
||||||
throw Exception(
|
throw Exception(tr('err.userfront.session.missing'));
|
||||||
tr('err.userfront.session.missing'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/verify-code');
|
final url = Uri.parse('$_baseUrl/api/v1/user/me/verify-code');
|
||||||
|
|||||||
@@ -232,13 +232,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
});
|
});
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(tr('msg.userfront.profile.phone.code_sent'))),
|
||||||
content: Text(
|
|
||||||
tr(
|
|
||||||
'msg.userfront.profile.phone.code_sent',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -272,11 +266,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
});
|
});
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(tr('msg.userfront.profile.phone.verified'))),
|
||||||
content: Text(
|
|
||||||
tr('msg.userfront.profile.phone.verified'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (_editingField == 'phone') {
|
if (_editingField == 'phone') {
|
||||||
@@ -315,17 +305,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
}
|
}
|
||||||
if (newPassword.isEmpty) {
|
if (newPassword.isEmpty) {
|
||||||
setState(
|
setState(
|
||||||
() => _passwordError = tr(
|
() =>
|
||||||
'msg.userfront.profile.password.new_required',
|
_passwordError = tr('msg.userfront.profile.password.new_required'),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newPassword != confirmPassword) {
|
if (newPassword != confirmPassword) {
|
||||||
setState(
|
setState(
|
||||||
() => _passwordError = tr(
|
() => _passwordError = tr('msg.userfront.profile.password.mismatch'),
|
||||||
'msg.userfront.profile.password.mismatch',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -347,9 +334,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
_newPasswordController?.clear();
|
_newPasswordController?.clear();
|
||||||
_confirmPasswordController?.clear();
|
_confirmPasswordController?.clear();
|
||||||
setState(() {
|
setState(() {
|
||||||
_passwordSuccess = tr(
|
_passwordSuccess = tr('msg.userfront.profile.password.changed');
|
||||||
'msg.userfront.profile.password.changed',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final message = e.toString().replaceFirst('Exception: ', '');
|
final message = e.toString().replaceFirst('Exception: ', '');
|
||||||
@@ -431,22 +416,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
|
|
||||||
if (_editingField == 'name' && nextName.isEmpty) {
|
if (_editingField == 'name' && nextName.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(tr('msg.userfront.profile.name_required'))),
|
||||||
content: Text(
|
|
||||||
tr('msg.userfront.profile.name_required'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_editingField == 'department' && nextDepartment.isEmpty) {
|
if (_editingField == 'department' && nextDepartment.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(tr('msg.userfront.profile.department_required')),
|
||||||
tr(
|
|
||||||
'msg.userfront.profile.department_required',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -454,24 +431,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
if (_editingField == 'phone') {
|
if (_editingField == 'phone') {
|
||||||
if (nextPhone.isEmpty) {
|
if (nextPhone.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(tr('msg.userfront.profile.phone_required'))),
|
||||||
content: Text(
|
|
||||||
tr(
|
|
||||||
'msg.userfront.profile.phone_required',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_isPhoneChanged && !_isPhoneVerified) {
|
if (_isPhoneChanged && !_isPhoneVerified) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(tr('msg.userfront.profile.phone_verify_required')),
|
||||||
tr(
|
|
||||||
'msg.userfront.profile.phone_verify_required',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -511,13 +478,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
_departmentTouched = false;
|
_departmentTouched = false;
|
||||||
});
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(tr('msg.userfront.profile.update_success'))),
|
||||||
content: Text(
|
|
||||||
tr(
|
|
||||||
'msg.userfront.profile.update_success',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -657,10 +618,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.profile.greeting', params: {'name': name}),
|
||||||
'msg.userfront.profile.greeting',
|
|
||||||
params: {'name': name},
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
@@ -833,9 +791,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
_isCodeSent
|
_isCodeSent
|
||||||
? tr('ui.common.resend')
|
? tr('ui.common.resend')
|
||||||
: tr(
|
: tr('ui.userfront.profile.phone.request_code'),
|
||||||
'ui.userfront.profile.phone.request_code',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -859,9 +815,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
onSubmitted: (_) => _verifyCode(profile),
|
onSubmitted: (_) => _verifyCode(profile),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
hintText: tr(
|
hintText: tr('ui.userfront.profile.phone.code_hint'),
|
||||||
'ui.userfront.profile.phone.code_hint',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -877,9 +831,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr('msg.userfront.profile.phone.verify_notice'),
|
||||||
'msg.userfront.profile.phone.verify_notice',
|
|
||||||
),
|
|
||||||
style: const TextStyle(color: Colors.orange, fontSize: 12),
|
style: const TextStyle(color: Colors.orange, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -898,9 +850,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.profile.password.subtitle'),
|
||||||
'msg.userfront.profile.password.subtitle',
|
|
||||||
),
|
|
||||||
style: const TextStyle(color: Color(0xFF6B7280)),
|
style: const TextStyle(color: Color(0xFF6B7280)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -908,9 +858,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
controller: _currentPasswordController,
|
controller: _currentPasswordController,
|
||||||
obscureText: !_showCurrentPassword,
|
obscureText: !_showCurrentPassword,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.profile.password.current'),
|
||||||
'ui.userfront.profile.password.current',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -929,9 +877,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
controller: _newPasswordController,
|
controller: _newPasswordController,
|
||||||
obscureText: !_showNewPassword,
|
obscureText: !_showNewPassword,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.profile.password.new'),
|
||||||
'ui.userfront.profile.password.new',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -948,9 +894,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
controller: _confirmPasswordController,
|
controller: _confirmPasswordController,
|
||||||
obscureText: !_showConfirmPassword,
|
obscureText: !_showConfirmPassword,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr(
|
labelText: tr('ui.userfront.profile.password.confirm'),
|
||||||
'ui.userfront.profile.password.confirm',
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -986,20 +930,12 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
height: 18,
|
height: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(tr('ui.userfront.profile.password.change')),
|
||||||
tr(
|
|
||||||
'ui.userfront.profile.password.change',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.go('/recovery'),
|
onPressed: () => context.go('/recovery'),
|
||||||
child: Text(
|
child: Text(tr('ui.userfront.profile.password.forgot')),
|
||||||
tr(
|
|
||||||
'ui.userfront.profile.password.forgot',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1024,9 +960,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_buildSectionTitle(
|
_buildSectionTitle(
|
||||||
tr('ui.userfront.profile.section.basic'),
|
tr('ui.userfront.profile.section.basic'),
|
||||||
tr(
|
tr('msg.userfront.profile.section.basic'),
|
||||||
'msg.userfront.profile.section.basic',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildCard(
|
_buildCard(
|
||||||
@@ -1034,9 +968,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
children: [
|
children: [
|
||||||
_buildEditableTile(
|
_buildEditableTile(
|
||||||
field: 'name',
|
field: 'name',
|
||||||
label: tr(
|
label: tr('ui.userfront.profile.field.name'),
|
||||||
'ui.userfront.profile.field.name',
|
|
||||||
),
|
|
||||||
value: profile.name,
|
value: profile.name,
|
||||||
profile: profile,
|
profile: profile,
|
||||||
isUpdating: isUpdating,
|
isUpdating: isUpdating,
|
||||||
@@ -1044,9 +976,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile(
|
_buildReadOnlyTile(
|
||||||
tr(
|
tr('ui.userfront.profile.field.email'),
|
||||||
'ui.userfront.profile.field.email',
|
|
||||||
),
|
|
||||||
profile.email,
|
profile.email,
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
@@ -1056,12 +986,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_buildSectionTitle(
|
_buildSectionTitle(
|
||||||
tr(
|
tr('ui.userfront.profile.section.organization'),
|
||||||
'ui.userfront.profile.section.organization',
|
tr('msg.userfront.profile.section.organization'),
|
||||||
),
|
|
||||||
tr(
|
|
||||||
'msg.userfront.profile.section.organization',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildCard(
|
_buildCard(
|
||||||
@@ -1069,9 +995,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
children: [
|
children: [
|
||||||
_buildEditableTile(
|
_buildEditableTile(
|
||||||
field: 'department',
|
field: 'department',
|
||||||
label: tr(
|
label: tr('ui.userfront.profile.field.department'),
|
||||||
'ui.userfront.profile.field.department',
|
|
||||||
),
|
|
||||||
value: profile.department,
|
value: profile.department,
|
||||||
profile: profile,
|
profile: profile,
|
||||||
isUpdating: isUpdating,
|
isUpdating: isUpdating,
|
||||||
@@ -1079,26 +1003,20 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile(
|
_buildReadOnlyTile(
|
||||||
tr(
|
tr('ui.userfront.profile.field.affiliation'),
|
||||||
'ui.userfront.profile.field.affiliation',
|
|
||||||
),
|
|
||||||
profile.affiliationType,
|
profile.affiliationType,
|
||||||
),
|
),
|
||||||
if (profile.tenant != null) ...[
|
if (profile.tenant != null) ...[
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile(
|
_buildReadOnlyTile(
|
||||||
tr(
|
tr('ui.userfront.profile.field.tenant'),
|
||||||
'ui.userfront.profile.field.tenant',
|
|
||||||
),
|
|
||||||
profile.tenant!.name,
|
profile.tenant!.name,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (profile.companyCode.isNotEmpty) ...[
|
if (profile.companyCode.isNotEmpty) ...[
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile(
|
_buildReadOnlyTile(
|
||||||
tr(
|
tr('ui.userfront.profile.field.company_code'),
|
||||||
'ui.userfront.profile.field.company_code',
|
|
||||||
),
|
|
||||||
profile.companyCode,
|
profile.companyCode,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1108,9 +1026,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
_buildSectionTitle(
|
_buildSectionTitle(
|
||||||
tr('ui.userfront.profile.section.security'),
|
tr('ui.userfront.profile.section.security'),
|
||||||
tr(
|
tr('msg.userfront.profile.section.security'),
|
||||||
'msg.userfront.profile.section.security',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildPasswordSection(),
|
_buildPasswordSection(),
|
||||||
@@ -1137,20 +1053,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
final profile = profileState.value ?? _cachedProfile;
|
final profile = profileState.value ?? _cachedProfile;
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(tr('ui.userfront.nav.profile'))),
|
||||||
title: Text(tr('ui.userfront.nav.profile')),
|
|
||||||
),
|
|
||||||
body: profileState.isLoading
|
body: profileState.isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: Center(
|
: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(tr('msg.userfront.profile.load_failed')),
|
||||||
tr(
|
|
||||||
'msg.userfront.profile.load_failed',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -131,9 +131,11 @@ final _router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'signin',
|
path: 'signin',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
final loginChallenge =
|
||||||
final redirectUrl = state.uri.queryParameters['redirect_uri'] ??
|
state.uri.queryParameters['login_challenge'];
|
||||||
state.uri.queryParameters['redirect_url'];
|
final redirectUrl =
|
||||||
|
state.uri.queryParameters['redirect_uri'] ??
|
||||||
|
state.uri.queryParameters['redirect_url'];
|
||||||
return LoginScreen(
|
return LoginScreen(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
loginChallenge: loginChallenge,
|
loginChallenge: loginChallenge,
|
||||||
@@ -145,9 +147,11 @@ final _router = GoRouter(
|
|||||||
path: 'login',
|
path: 'login',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// IMPORTANT: Match signin logic to handle OIDC challenges
|
// IMPORTANT: Match signin logic to handle OIDC challenges
|
||||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
final loginChallenge =
|
||||||
final redirectUrl = state.uri.queryParameters['redirect_uri'] ??
|
state.uri.queryParameters['login_challenge'];
|
||||||
state.uri.queryParameters['redirect_url'];
|
final redirectUrl =
|
||||||
|
state.uri.queryParameters['redirect_uri'] ??
|
||||||
|
state.uri.queryParameters['redirect_url'];
|
||||||
return LoginScreen(
|
return LoginScreen(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
loginChallenge: loginChallenge,
|
loginChallenge: loginChallenge,
|
||||||
@@ -158,10 +162,13 @@ final _router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'consent',
|
path: 'consent',
|
||||||
builder: (BuildContext context, GoRouterState state) {
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
final consentChallenge = state.uri.queryParameters['consent_challenge'];
|
final consentChallenge =
|
||||||
|
state.uri.queryParameters['consent_challenge'];
|
||||||
if (consentChallenge == null) {
|
if (consentChallenge == null) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
body: Center(child: Text('Error: Consent challenge is missing.')),
|
body: Center(
|
||||||
|
child: Text('Error: Consent challenge is missing.'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ConsentScreen(consentChallenge: consentChallenge);
|
return ConsentScreen(consentChallenge: consentChallenge);
|
||||||
@@ -231,15 +238,13 @@ final _router = GoRouter(
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'approve',
|
path: 'approve',
|
||||||
builder: (context, state) => ApproveQrScreen(
|
builder: (context, state) =>
|
||||||
pendingRef: state.uri.queryParameters['ref'],
|
ApproveQrScreen(pendingRef: state.uri.queryParameters['ref']),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'ql/:ref',
|
path: 'ql/:ref',
|
||||||
builder: (context, state) => ApproveQrScreen(
|
builder: (context, state) =>
|
||||||
pendingRef: state.pathParameters['ref'],
|
ApproveQrScreen(pendingRef: state.pathParameters['ref']),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'scan',
|
path: 'scan',
|
||||||
@@ -265,7 +270,8 @@ final _router = GoRouter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
final token = AuthTokenStore.getToken();
|
final token = AuthTokenStore.getToken();
|
||||||
final isLoggedIn = (token != null && token.isNotEmpty) || AuthTokenStore.usesCookie();
|
final isLoggedIn =
|
||||||
|
(token != null && token.isNotEmpty) || AuthTokenStore.usesCookie();
|
||||||
final path = stripLocalePath(uri);
|
final path = stripLocalePath(uri);
|
||||||
|
|
||||||
// Precise public path detection
|
// Precise public path detection
|
||||||
|
|||||||
Reference in New Issue
Block a user