forked from baron/baron-sso
ci: enforce flutter/biome format checks and fix front lint violations
This commit is contained in:
@@ -75,8 +75,8 @@ jobs:
|
||||
- name: Biome check adminfront (lint + format)
|
||||
run: |
|
||||
cd adminfront
|
||||
npx biome lint .
|
||||
npx biome format . --check
|
||||
npx biome check . --formatter-enabled=false --organize-imports-enabled=false
|
||||
npx biome check . --linter-enabled=false --organize-imports-enabled=false
|
||||
|
||||
- name: Install devfront dependencies
|
||||
run: |
|
||||
@@ -86,8 +86,8 @@ jobs:
|
||||
- name: Biome check devfront (lint + format)
|
||||
run: |
|
||||
cd devfront
|
||||
npx biome lint .
|
||||
npx biome format . --check
|
||||
npx biome check . --formatter-enabled=false --organize-imports-enabled=false
|
||||
npx biome check . --linter-enabled=false --organize-imports-enabled=false
|
||||
|
||||
- name: Lint Go backend
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import { fetchGroups, fetchTenants } from "../../../lib/adminApi";
|
||||
import {
|
||||
fetchGroups,
|
||||
fetchTenants,
|
||||
type TenantSummary,
|
||||
} from "../../../lib/adminApi";
|
||||
|
||||
export default function GlobalUserGroupListPage() {
|
||||
const { data: tenantList, isLoading: isTenantsLoading } = useQuery({
|
||||
@@ -51,7 +55,7 @@ export default function GlobalUserGroupListPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function TenantGroupCard({ tenant }: { tenant: any }) {
|
||||
function TenantGroupCard({ tenant }: { tenant: TenantSummary }) {
|
||||
const { data: groups, isLoading } = useQuery({
|
||||
queryKey: ["tenant-user-groups", tenant.id],
|
||||
queryFn: () => fetchGroups(tenant.id),
|
||||
|
||||
@@ -31,6 +31,21 @@ import {
|
||||
} from "../../../components/ui/table";
|
||||
import { createGroup, deleteGroup, fetchGroups } from "../../../lib/adminApi";
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"message" in error &&
|
||||
typeof (error as { message?: unknown }).message === "string"
|
||||
) {
|
||||
return (error as { message: string }).message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function TenantUserGroupsTab() {
|
||||
const { tenantId } = useParams<{ tenantId: string }>();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -40,13 +55,25 @@ export function TenantUserGroupsTab() {
|
||||
|
||||
const { data: groups, isLoading } = useQuery({
|
||||
queryKey: ["tenant-user-groups", tenantId],
|
||||
queryFn: () => fetchGroups(tenantId!),
|
||||
queryFn: () => {
|
||||
if (!tenantId) {
|
||||
throw new Error("tenantId is required");
|
||||
}
|
||||
return fetchGroups(tenantId);
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createGroup(tenantId!, { name: newGroupName, description: newGroupDesc }),
|
||||
mutationFn: () => {
|
||||
if (!tenantId) {
|
||||
throw new Error("tenantId is required");
|
||||
}
|
||||
return createGroup(tenantId, {
|
||||
name: newGroupName,
|
||||
description: newGroupDesc,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["tenant-user-groups", tenantId],
|
||||
@@ -56,13 +83,18 @@ export function TenantUserGroupsTab() {
|
||||
setNewGroupDesc("");
|
||||
alert("User group created successfully");
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert(error.message || "Failed to create user group");
|
||||
onError: (error: unknown) => {
|
||||
alert(getErrorMessage(error, "Failed to create user group"));
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (groupId: string) => deleteGroup(tenantId!, groupId),
|
||||
mutationFn: (groupId: string) => {
|
||||
if (!tenantId) {
|
||||
throw new Error("tenantId is required");
|
||||
}
|
||||
return deleteGroup(tenantId, groupId);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["tenant-user-groups", tenantId],
|
||||
|
||||
@@ -48,6 +48,28 @@ import {
|
||||
removeGroupRole,
|
||||
} from "../../../lib/adminApi";
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string): string {
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const response = (error as { response?: { data?: { error?: unknown } } })
|
||||
.response;
|
||||
const responseError = response?.data?.error;
|
||||
if (typeof responseError === "string" && responseError.length > 0) {
|
||||
return responseError;
|
||||
}
|
||||
|
||||
const message = (error as { message?: unknown }).message;
|
||||
if (typeof message === "string" && message.length > 0) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function UserGroupDetailPage() {
|
||||
const { tenantId, id } = useParams<{ tenantId: string; id: string }>();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -67,7 +89,12 @@ export function UserGroupDetailPage() {
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["user-group-detail", id],
|
||||
queryFn: () => fetchGroup(tenantId!, id!),
|
||||
queryFn: () => {
|
||||
if (!tenantId || !id) {
|
||||
throw new Error("tenantId and id are required");
|
||||
}
|
||||
return fetchGroup(tenantId, id);
|
||||
},
|
||||
enabled: !!id && !!tenantId,
|
||||
retry: false,
|
||||
});
|
||||
@@ -75,7 +102,12 @@ export function UserGroupDetailPage() {
|
||||
// Fetch assigned roles
|
||||
const { data: groupRoles, isLoading: isRolesLoading } = useQuery({
|
||||
queryKey: ["user-group-roles", id],
|
||||
queryFn: () => fetchGroupRoles(tenantId!, id!),
|
||||
queryFn: () => {
|
||||
if (!tenantId || !id) {
|
||||
throw new Error("tenantId and id are required");
|
||||
}
|
||||
return fetchGroupRoles(tenantId, id);
|
||||
},
|
||||
enabled: !!id && !!tenantId,
|
||||
});
|
||||
|
||||
@@ -94,20 +126,30 @@ export function UserGroupDetailPage() {
|
||||
});
|
||||
|
||||
const addMemberMutation = useMutation({
|
||||
mutationFn: (userId: string) => addGroupMember(tenantId!, id!, userId),
|
||||
mutationFn: (userId: string) => {
|
||||
if (!tenantId || !id) {
|
||||
throw new Error("tenantId and id are required");
|
||||
}
|
||||
return addGroupMember(tenantId, id, userId);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user-group-detail", id] });
|
||||
setIsAddMemberOpen(false);
|
||||
setSelectedUserId("");
|
||||
alert("Member added successfully");
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert(error.message || "Failed to add member");
|
||||
onError: (error: unknown) => {
|
||||
alert(getErrorMessage(error, "Failed to add member"));
|
||||
},
|
||||
});
|
||||
|
||||
const removeMemberMutation = useMutation({
|
||||
mutationFn: (userId: string) => removeGroupMember(tenantId!, id!, userId),
|
||||
mutationFn: (userId: string) => {
|
||||
if (!tenantId || !id) {
|
||||
throw new Error("tenantId and id are required");
|
||||
}
|
||||
return removeGroupMember(tenantId, id, userId);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user-group-detail", id] });
|
||||
alert("Member removed successfully");
|
||||
@@ -115,21 +157,34 @@ export function UserGroupDetailPage() {
|
||||
});
|
||||
|
||||
const assignRoleMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
assignGroupRole(tenantId!, id!, selectedTargetTenantId, selectedRelation),
|
||||
mutationFn: () => {
|
||||
if (!tenantId || !id) {
|
||||
throw new Error("tenantId and id are required");
|
||||
}
|
||||
return assignGroupRole(
|
||||
tenantId,
|
||||
id,
|
||||
selectedTargetTenantId,
|
||||
selectedRelation,
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
|
||||
setIsAddRoleOpen(false);
|
||||
alert(`Role '${selectedRelation}' assigned successfully`);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert(error.message || "Failed to assign role");
|
||||
onError: (error: unknown) => {
|
||||
alert(getErrorMessage(error, "Failed to assign role"));
|
||||
},
|
||||
});
|
||||
|
||||
const removeRoleMutation = useMutation({
|
||||
mutationFn: (role: { targetTenantId: string; relation: string }) =>
|
||||
removeGroupRole(tenantId!, id!, role.targetTenantId, role.relation),
|
||||
mutationFn: (role: { targetTenantId: string; relation: string }) => {
|
||||
if (!tenantId || !id) {
|
||||
throw new Error("tenantId and id are required");
|
||||
}
|
||||
return removeGroupRole(tenantId, id, role.targetTenantId, role.relation);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user-group-roles", id] });
|
||||
alert("Role removed successfully");
|
||||
@@ -139,7 +194,7 @@ export function UserGroupDetailPage() {
|
||||
if (isGroupLoading)
|
||||
return (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
<span className="ml-3 text-muted-foreground">
|
||||
Loading group details...
|
||||
</span>
|
||||
@@ -153,12 +208,7 @@ export function UserGroupDetailPage() {
|
||||
Could not load group
|
||||
</h3>
|
||||
<div className="p-4 bg-red-50 text-red-700 rounded-md text-left text-sm font-mono overflow-auto max-w-xl mx-auto border border-red-100">
|
||||
<p>
|
||||
Error:{" "}
|
||||
{(error as any)?.response?.data?.error ||
|
||||
(error as any)?.message ||
|
||||
"Not found"}
|
||||
</p>
|
||||
<p>Error: {getErrorMessage(error, "Not found")}</p>
|
||||
<p className="mt-2 text-red-500 opacity-70">
|
||||
Path: /admin/tenants/{tenantId}/user-groups/{id}
|
||||
</p>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { UserManager, WebStorageStateStore } from "oidc-client-ts";
|
||||
import type { AuthProviderProps } from "react-oidc-context";
|
||||
|
||||
export const oidcConfig: AuthProviderProps = {
|
||||
authority: import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
|
||||
authority:
|
||||
import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
|
||||
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "adminfront",
|
||||
redirect_uri: `${window.location.origin}/auth/callback`,
|
||||
response_type: "code",
|
||||
|
||||
@@ -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"
|
||||
aria-label={t("ui.common.language", "언어")}
|
||||
>
|
||||
<option value="ko">
|
||||
{t("ui.common.language_ko", "한국어")}
|
||||
</option>
|
||||
<option value="en">
|
||||
{t("ui.common.language_en", "English")}
|
||||
</option>
|
||||
<option value="ko">{t("ui.common.language_ko", "한국어")}</option>
|
||||
<option value="en">{t("ui.common.language_en", "English")}</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ const badgeVariants = cva(
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
|
||||
@@ -34,8 +34,7 @@ const buttonVariants = cva(
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
|
||||
@@ -35,18 +35,18 @@ function LoginPage() {
|
||||
<Card className="border-primary/20 bg-card/50 backdrop-blur-xl shadow-2xl">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl flex items-center gap-2">
|
||||
<LogIn size={20} className="text-primary" />
|
||||
개발자 포털 로그인
|
||||
<LogIn size={20} className="text-primary" />
|
||||
개발자 포털 로그인
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Baron 통합 인증(SSO)을 통해 개발자 포털에 접속합니다.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 pb-8 space-y-3">
|
||||
<Button
|
||||
onClick={handleSSOLogin}
|
||||
className="w-full h-14 text-lg font-semibold flex gap-3 shadow-lg"
|
||||
disabled={auth.isLoading}
|
||||
<Button
|
||||
onClick={handleSSOLogin}
|
||||
className="w-full h-14 text-lg font-semibold flex gap-3 shadow-lg"
|
||||
disabled={auth.isLoading}
|
||||
>
|
||||
{auth.isLoading ? (
|
||||
<>
|
||||
@@ -61,22 +61,24 @@ function LoginPage() {
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
|
||||
<p className="mt-6 text-xs text-center text-muted-foreground leading-relaxed">
|
||||
개발자 포털 세션은 브라우저 정책에 따라 유지됩니다.<br />
|
||||
개발자 포털 세션은 브라우저 정책에 따라 유지됩니다.
|
||||
<br />
|
||||
민감한 작업 시 재인증을 요구할 수 있습니다.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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 className="h-1 w-1 rounded-full bg-primary/30" />
|
||||
<div className="h-1 w-1 rounded-full bg-primary/30" />
|
||||
</div>
|
||||
|
||||
<p className="px-8 text-center text-sm text-muted-foreground">
|
||||
인증 정보가 없거나 로그인이 되지 않는 경우<br />
|
||||
인증 정보가 없거나 로그인이 되지 않는 경우
|
||||
<br />
|
||||
시스템 관리자에게 문의하세요.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -208,11 +208,7 @@ function ClientsPage() {
|
||||
<div className="mt-1 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold">{item.value}</span>
|
||||
<Badge
|
||||
variant={
|
||||
item.tone === "up"
|
||||
? "success"
|
||||
: "muted"
|
||||
}
|
||||
variant={item.tone === "up" ? "success" : "muted"}
|
||||
className={cn(
|
||||
"px-2",
|
||||
item.tone === "stable" && "bg-muted/40 text-foreground",
|
||||
|
||||
@@ -54,8 +54,7 @@
|
||||
|
||||
body {
|
||||
@apply min-h-screen bg-background font-sans text-foreground antialiased;
|
||||
background-image:
|
||||
radial-gradient(
|
||||
background-image: radial-gradient(
|
||||
circle at 10% 18%,
|
||||
rgba(54, 211, 153, 0.16),
|
||||
transparent 28%
|
||||
|
||||
@@ -2,7 +2,8 @@ import { UserManager, WebStorageStateStore } from "oidc-client-ts";
|
||||
import type { AuthProviderProps } from "react-oidc-context";
|
||||
|
||||
export const oidcConfig: AuthProviderProps = {
|
||||
authority: import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
|
||||
authority:
|
||||
import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:5000/oidc", // Gateway Proxy URL
|
||||
client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "devfront",
|
||||
redirect_uri: `${window.location.origin}/auth/callback`,
|
||||
response_type: "code",
|
||||
|
||||
@@ -34,16 +34,12 @@ class _LocaleGateState extends State<LocaleGate> {
|
||||
Future<void> _applyLocale() async {
|
||||
final normalized = normalizeLocaleCode(widget.localeCode);
|
||||
LocaleStorage.write(normalized);
|
||||
webWindow.setTitle(
|
||||
tr('ui.userfront.app_title'),
|
||||
);
|
||||
webWindow.setTitle(tr('ui.userfront.app_title'));
|
||||
if (context.locale.languageCode == normalized) {
|
||||
return;
|
||||
}
|
||||
await context.setLocale(Locale(normalized));
|
||||
webWindow.setTitle(
|
||||
tr('ui.userfront.app_title'),
|
||||
);
|
||||
webWindow.setTitle(tr('ui.userfront.app_title'));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -76,7 +76,7 @@ String buildLocalizedPath(String localeCode, Uri uri) {
|
||||
}
|
||||
}
|
||||
final newPath = '/${[localeCode, ...restSegments].join('/')}';
|
||||
|
||||
|
||||
// Return only the path and query part to avoid GoRouter confusion with full URLs
|
||||
final newUri = uri.replace(path: newPath);
|
||||
String result = newUri.path;
|
||||
|
||||
@@ -299,10 +299,7 @@ class AuthProxyService {
|
||||
} else {
|
||||
final errorBody = jsonDecode(response.body);
|
||||
throw Exception(
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_fetch',
|
||||
),
|
||||
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_fetch'),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -333,10 +330,7 @@ class AuthProxyService {
|
||||
} else {
|
||||
final errorBody = jsonDecode(response.body);
|
||||
throw Exception(
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_accept',
|
||||
),
|
||||
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_accept'),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -363,10 +357,7 @@ class AuthProxyService {
|
||||
} else {
|
||||
final errorBody = jsonDecode(response.body);
|
||||
throw Exception(
|
||||
errorBody['error'] ??
|
||||
tr(
|
||||
'err.userfront.auth_proxy.consent_reject',
|
||||
),
|
||||
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_reject'),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -15,10 +15,7 @@ class LanguageSelector extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final current = context.locale.languageCode;
|
||||
final items = [
|
||||
DropdownMenuItem(
|
||||
value: 'ko',
|
||||
child: Text(tr('ui.common.language_ko')),
|
||||
),
|
||||
DropdownMenuItem(value: 'ko', child: Text(tr('ui.common.language_ko'))),
|
||||
DropdownMenuItem(
|
||||
value: 'en',
|
||||
child: Text(tr('ui.common.language_en', fallback: 'English')),
|
||||
|
||||
@@ -39,9 +39,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
'msg.userfront.error.title_with_code',
|
||||
params: {'code': normalizedCode},
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.error.title_generic',
|
||||
));
|
||||
: tr('msg.userfront.error.title_generic'));
|
||||
final detail = isProd
|
||||
? (isInternalWhitelisted
|
||||
? tr(
|
||||
@@ -51,23 +49,16 @@ class ErrorScreen extends StatelessWidget {
|
||||
: (isOryBypass
|
||||
? tr(
|
||||
'msg.userfront.error.ory.$normalizedCode',
|
||||
fallback:
|
||||
(description?.isNotEmpty == true)
|
||||
? description
|
||||
: tr('msg.userfront.error.detail_request'),
|
||||
fallback: (description?.isNotEmpty == true)
|
||||
? description
|
||||
: tr('msg.userfront.error.detail_request'),
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.error.detail_contact',
|
||||
)))
|
||||
: tr('msg.userfront.error.detail_contact')))
|
||||
: ((description?.isNotEmpty == true)
|
||||
? description!
|
||||
: (hasCode
|
||||
? tr(
|
||||
'msg.userfront.error.detail_generic',
|
||||
)
|
||||
: tr(
|
||||
'msg.userfront.error.detail_request',
|
||||
)));
|
||||
? tr('msg.userfront.error.detail_generic')
|
||||
: tr('msg.userfront.error.detail_request')));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F8FA),
|
||||
@@ -104,10 +95,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.error.type',
|
||||
params: {'type': errorType},
|
||||
),
|
||||
tr('msg.userfront.error.type', params: {'type': errorType}),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: const Color(0xFF6B7280),
|
||||
),
|
||||
@@ -115,10 +103,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
if (errorId != null && errorId!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.error.id',
|
||||
params: {'id': errorId!},
|
||||
),
|
||||
tr('msg.userfront.error.id', params: {'id': errorId!}),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: const Color(0xFF6B7280),
|
||||
),
|
||||
@@ -142,11 +127,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.error.go_login',
|
||||
),
|
||||
),
|
||||
child: Text(tr('ui.userfront.error.go_login')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => context.go('/'),
|
||||
@@ -161,9 +142,7 @@ class ErrorScreen extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
tr('ui.userfront.error.go_home'),
|
||||
),
|
||||
child: Text(tr('ui.userfront.error.go_home')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -25,11 +25,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
Future<void> _handlePasswordReset() async {
|
||||
final input = _loginIdController.text.trim();
|
||||
if (input.isEmpty) {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.forgot.input_required',
|
||||
),
|
||||
);
|
||||
_showError(tr('msg.userfront.forgot.input_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,11 +48,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.forgot.sent',
|
||||
),
|
||||
),
|
||||
content: Text(tr('msg.userfront.forgot.sent')),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
@@ -65,10 +57,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.forgot.error',
|
||||
params: {'error': e.toString()},
|
||||
),
|
||||
tr('msg.userfront.forgot.error', params: {'error': e.toString()}),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -133,9 +122,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.forgot.dry_send',
|
||||
),
|
||||
tr('msg.userfront.forgot.dry_send'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF8A6D3B),
|
||||
fontSize: 12,
|
||||
@@ -148,9 +135,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.forgot.description',
|
||||
),
|
||||
tr('msg.userfront.forgot.description'),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
@@ -158,9 +143,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
TextField(
|
||||
controller: _loginIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.forgot.input_label',
|
||||
),
|
||||
labelText: tr('ui.userfront.forgot.input_label'),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
),
|
||||
@@ -181,9 +164,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
tr('ui.userfront.forgot.submit'),
|
||||
),
|
||||
: Text(tr('ui.userfront.forgot.submit')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -900,8 +900,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
_onLoginSuccess(jwt, provider: provider, redirectTo: redirectTo);
|
||||
} else if (redirectTo != null && redirectTo.isNotEmpty) {
|
||||
webWindow.redirectTo(redirectTo);
|
||||
} else {
|
||||
}
|
||||
} else {}
|
||||
} catch (e) {
|
||||
if (e.toString().contains("User not registered")) {
|
||||
_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 {
|
||||
if (!mounted) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
// [Priority 1] Immediate External Redirection
|
||||
@@ -1139,7 +1141,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
} catch (stErr) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
|
||||
webWindow.redirectTo(redirectTo); // Removed await as it's void
|
||||
return;
|
||||
}
|
||||
@@ -1150,24 +1152,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
// Save token first, it's needed for acceptance
|
||||
final providerName = provider ?? AuthTokenStore.getProvider();
|
||||
AuthTokenStore.setToken(token, provider: providerName);
|
||||
|
||||
|
||||
final res = await AuthProxyService.acceptOidcLogin(
|
||||
_loginChallenge!,
|
||||
token: token,
|
||||
);
|
||||
final nextRedirectTo = res['redirectTo'] as String?;
|
||||
|
||||
|
||||
if (nextRedirectTo != null && nextRedirectTo.isNotEmpty) {
|
||||
webWindow.redirectTo(nextRedirectTo); // Removed await
|
||||
return;
|
||||
} else {
|
||||
}
|
||||
} else {}
|
||||
} catch (e) {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.login.oidc_failed',
|
||||
),
|
||||
);
|
||||
_showError(tr('msg.userfront.login.oidc_failed'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1188,7 +1185,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
|
||||
final uri = Uri.base;
|
||||
final redirectParam =
|
||||
uri.queryParameters['redirect_uri'] ?? uri.queryParameters['redirect_url'];
|
||||
uri.queryParameters['redirect_uri'] ??
|
||||
uri.queryParameters['redirect_url'];
|
||||
final hasRedirectParam =
|
||||
redirectParam != null && redirectParam.isNotEmpty;
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.login_success.subtitle',
|
||||
),
|
||||
tr('msg.userfront.login_success.subtitle'),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||||
),
|
||||
@@ -40,11 +38,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
||||
context.push('/scan');
|
||||
},
|
||||
icon: const Icon(Icons.camera_alt, size: 28),
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.login_success.qr',
|
||||
),
|
||||
),
|
||||
label: Text(tr('ui.userfront.login_success.qr')),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(80), // 버튼 높이를 더 크게
|
||||
backgroundColor: Colors.blue.shade700,
|
||||
@@ -63,9 +57,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
||||
context.go('/');
|
||||
},
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.login_success.later',
|
||||
),
|
||||
tr('ui.userfront.login_success.later'),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,7 +21,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
||||
),
|
||||
),
|
||||
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 ((_loginId == null || _loginId!.isEmpty) &&
|
||||
(_token == null || _token!.isEmpty)) {
|
||||
_showError(
|
||||
tr(
|
||||
'msg.userfront.reset.invalid_link',
|
||||
),
|
||||
);
|
||||
_showError(tr('msg.userfront.reset.invalid_link'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,11 +85,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.reset.success',
|
||||
),
|
||||
),
|
||||
content: Text(tr('msg.userfront.reset.success')),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
@@ -123,9 +115,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
|
||||
String _buildPolicyDescription() {
|
||||
if (_isPolicyLoading) {
|
||||
return tr(
|
||||
'msg.userfront.reset.policy_loading',
|
||||
);
|
||||
return tr('msg.userfront.reset.policy_loading');
|
||||
}
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||
@@ -149,22 +139,16 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
);
|
||||
}
|
||||
if (requiresLower) {
|
||||
parts.add(
|
||||
tr('msg.userfront.reset.policy.lowercase'),
|
||||
);
|
||||
parts.add(tr('msg.userfront.reset.policy.lowercase'));
|
||||
}
|
||||
if (requiresUpper) {
|
||||
parts.add(
|
||||
tr('msg.userfront.reset.policy.uppercase'),
|
||||
);
|
||||
parts.add(tr('msg.userfront.reset.policy.uppercase'));
|
||||
}
|
||||
if (requiresNumber) {
|
||||
parts.add(tr('msg.userfront.reset.policy.number'));
|
||||
}
|
||||
if (requiresSymbol) {
|
||||
parts.add(
|
||||
tr('msg.userfront.reset.policy.symbol'),
|
||||
);
|
||||
parts.add(tr('msg.userfront.reset.policy.symbol'));
|
||||
}
|
||||
|
||||
return parts.join(", ");
|
||||
@@ -192,9 +176,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'ui.userfront.reset.subtitle',
|
||||
),
|
||||
tr('ui.userfront.reset.subtitle'),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -212,9 +194,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
controller: _passwordController,
|
||||
obscureText: _isPasswordObscured,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.reset.new_password',
|
||||
),
|
||||
labelText: tr('ui.userfront.reset.new_password'),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
suffixIcon: IconButton(
|
||||
@@ -265,25 +245,17 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
}
|
||||
|
||||
if ((_policy?['lowercase'] ?? true) && !hasLower) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.lowercase',
|
||||
);
|
||||
return tr('msg.userfront.reset.error.lowercase');
|
||||
}
|
||||
if ((_policy?['uppercase'] ?? false) && !hasUpper) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.uppercase',
|
||||
);
|
||||
return tr('msg.userfront.reset.error.uppercase');
|
||||
}
|
||||
if ((_policy?['number'] ?? true) && !hasNumber) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.number',
|
||||
);
|
||||
return tr('msg.userfront.reset.error.number');
|
||||
}
|
||||
if ((_policy?['nonAlphanumeric'] ?? true) &&
|
||||
!hasSymbol) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.symbol',
|
||||
);
|
||||
return tr('msg.userfront.reset.error.symbol');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -293,9 +265,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
controller: _confirmPasswordController,
|
||||
obscureText: _isConfirmPasswordObscured,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.reset.confirm_password',
|
||||
),
|
||||
labelText: tr('ui.userfront.reset.confirm_password'),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
suffixIcon: IconButton(
|
||||
@@ -314,9 +284,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
),
|
||||
validator: (value) {
|
||||
if (value != _passwordController.text) {
|
||||
return tr(
|
||||
'msg.userfront.reset.error.mismatch',
|
||||
);
|
||||
return tr('msg.userfront.reset.error.mismatch');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -336,11 +304,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
tr(
|
||||
'ui.userfront.reset.submit',
|
||||
),
|
||||
),
|
||||
: Text(tr('ui.userfront.reset.submit')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -364,9 +328,7 @@ class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.reset.invalid_body',
|
||||
),
|
||||
tr('msg.userfront.reset.invalid_body'),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -164,11 +164,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
final email = _emailController.text.trim();
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(email)) {
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.invalid',
|
||||
),
|
||||
);
|
||||
setState(() => _emailError = tr('msg.userfront.signup.email.invalid'));
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
@@ -179,9 +175,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
final available = await AuthProxyService.checkEmailAvailability(email);
|
||||
if (!available) {
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.duplicate',
|
||||
),
|
||||
() => _emailError = tr('msg.userfront.signup.email.duplicate'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -217,9 +211,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
});
|
||||
} else {
|
||||
setState(
|
||||
() => _emailError = tr(
|
||||
'msg.userfront.signup.email.code_mismatch',
|
||||
),
|
||||
() => _emailError = tr('msg.userfront.signup.email.code_mismatch'),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -272,9 +264,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
});
|
||||
} else {
|
||||
setState(
|
||||
() => _phoneError = tr(
|
||||
'msg.userfront.signup.phone.code_mismatch',
|
||||
),
|
||||
() => _phoneError = tr('msg.userfront.signup.phone.code_mismatch'),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -329,17 +319,11 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
'msg.userfront.signup.password.lowercase_required',
|
||||
);
|
||||
} else if (eStr.contains('digit') || eStr.contains('number')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.number_required',
|
||||
);
|
||||
_passwordError = tr('msg.userfront.signup.password.number_required');
|
||||
} else if (eStr.contains('symbol') || eStr.contains('special')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.symbol_required',
|
||||
);
|
||||
_passwordError = tr('msg.userfront.signup.password.symbol_required');
|
||||
} else if (eStr.contains('length') || eStr.contains('12 characters')) {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.password.length_required',
|
||||
);
|
||||
_passwordError = tr('msg.userfront.signup.password.length_required');
|
||||
} else {
|
||||
_passwordError = tr(
|
||||
'msg.userfront.signup.failed',
|
||||
@@ -357,18 +341,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
tr('msg.userfront.signup.success.title'),
|
||||
),
|
||||
content: Text(
|
||||
tr('msg.userfront.signup.success.body'),
|
||||
),
|
||||
title: Text(tr('msg.userfront.signup.success.title')),
|
||||
content: Text(tr('msg.userfront.signup.success.body')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.go('/signin'),
|
||||
child: Text(
|
||||
tr('ui.userfront.signup.success.action'),
|
||||
),
|
||||
child: Text(tr('ui.userfront.signup.success.action')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -382,25 +360,13 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
_stepCircle(
|
||||
1,
|
||||
tr('ui.userfront.signup.steps.agreement'),
|
||||
),
|
||||
_stepCircle(1, tr('ui.userfront.signup.steps.agreement')),
|
||||
_stepLine(1),
|
||||
_stepCircle(
|
||||
2,
|
||||
tr('ui.userfront.signup.steps.verify'),
|
||||
),
|
||||
_stepCircle(2, tr('ui.userfront.signup.steps.verify')),
|
||||
_stepLine(2),
|
||||
_stepCircle(
|
||||
3,
|
||||
tr('ui.userfront.signup.steps.profile'),
|
||||
),
|
||||
_stepCircle(3, tr('ui.userfront.signup.steps.profile')),
|
||||
_stepLine(3),
|
||||
_stepCircle(
|
||||
4,
|
||||
tr('ui.userfront.signup.steps.password'),
|
||||
),
|
||||
_stepCircle(4, tr('ui.userfront.signup.steps.password')),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -454,9 +420,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.agreement.title',
|
||||
),
|
||||
tr('msg.userfront.signup.agreement.title'),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -489,18 +453,14 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_agreementSection(
|
||||
title: tr(
|
||||
'ui.userfront.signup.agreement.tos_title',
|
||||
),
|
||||
title: tr('ui.userfront.signup.agreement.tos_title'),
|
||||
content: _tosText,
|
||||
value: _termsAccepted,
|
||||
onChanged: (val) => setState(() => _termsAccepted = val!),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_agreementSection(
|
||||
title: tr(
|
||||
'ui.userfront.signup.agreement.privacy_title',
|
||||
),
|
||||
title: tr('ui.userfront.signup.agreement.privacy_title'),
|
||||
content: _privacyText,
|
||||
value: _privacyAccepted,
|
||||
onChanged: (val) => setState(() => _privacyAccepted = val!),
|
||||
@@ -745,9 +705,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.auth.title',
|
||||
),
|
||||
tr('msg.userfront.signup.auth.title'),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -764,9 +722,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.auth.affiliate_notice',
|
||||
),
|
||||
tr('msg.userfront.signup.auth.affiliate_notice'),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue,
|
||||
@@ -790,9 +746,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
controller: _emailController,
|
||||
onChanged: _checkEmailAffiliation, // 도메인 실시간 체크
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.email.label',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.auth.email.label'),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _emailError,
|
||||
hintText: 'example@hanmaceng.co.kr',
|
||||
@@ -815,9 +769,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: Text(
|
||||
_emailSeconds > 0
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.signup.auth.request_code',
|
||||
),
|
||||
: tr('ui.userfront.signup.auth.request_code'),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -828,9 +780,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
TextFormField(
|
||||
controller: _emailCodeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.code_label',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.auth.code_label'),
|
||||
suffixText: _formatTime(_emailSeconds),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
@@ -848,9 +798,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.email.verified',
|
||||
),
|
||||
tr('msg.userfront.signup.email.verified'),
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 13,
|
||||
@@ -870,9 +818,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: TextFormField(
|
||||
controller: _phoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.phone.label',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.phone.label'),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _phoneError,
|
||||
),
|
||||
@@ -895,9 +841,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
child: Text(
|
||||
_phoneSeconds > 0
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.signup.auth.request_code',
|
||||
),
|
||||
: tr('ui.userfront.signup.auth.request_code'),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -908,9 +852,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
TextFormField(
|
||||
controller: _phoneCodeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.auth.code_label',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.auth.code_label'),
|
||||
suffixText: _formatTime(_phoneSeconds),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
@@ -928,9 +870,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.signup.phone.verified',
|
||||
),
|
||||
tr('msg.userfront.signup.phone.verified'),
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 13,
|
||||
@@ -947,9 +887,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.profile.title',
|
||||
),
|
||||
tr('msg.userfront.signup.profile.title'),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -971,28 +909,20 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
key: ValueKey(_affiliationType),
|
||||
initialValue: _affiliationType,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.profile.affiliation_type',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.profile.affiliation_type'),
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: _isAffiliateEmail
|
||||
? tr(
|
||||
'msg.userfront.signup.profile.affiliate_hint',
|
||||
)
|
||||
? tr('msg.userfront.signup.profile.affiliate_hint')
|
||||
: null,
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'GENERAL',
|
||||
child: Text(
|
||||
tr('domain.affiliation.general'),
|
||||
),
|
||||
child: Text(tr('domain.affiliation.general')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'AFFILIATE',
|
||||
child: Text(
|
||||
tr('domain.affiliation.affiliate'),
|
||||
),
|
||||
child: Text(tr('domain.affiliation.affiliate')),
|
||||
),
|
||||
],
|
||||
onChanged: _isAffiliateEmail
|
||||
@@ -1019,9 +949,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
key: ValueKey(_companyCode ?? 'none'),
|
||||
initialValue: _companyCode,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.profile.company',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.profile.company'),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
@@ -1064,9 +992,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
decoration: InputDecoration(
|
||||
labelText: _affiliationType == 'AFFILIATE'
|
||||
? tr('ui.userfront.signup.profile.department')
|
||||
: tr(
|
||||
'ui.userfront.signup.profile.department_optional',
|
||||
),
|
||||
: tr('ui.userfront.signup.profile.department_optional'),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
@@ -1076,9 +1002,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
|
||||
String _buildPolicyDescription() {
|
||||
if (_isPolicyLoading) {
|
||||
return tr(
|
||||
'msg.userfront.signup.policy.loading',
|
||||
);
|
||||
return tr('msg.userfront.signup.policy.loading');
|
||||
}
|
||||
final minLength = (_policy?['minLength'] as int?) ?? 12;
|
||||
final minTypes = (_policy?['minCharacterTypes'] as int?) ?? 0;
|
||||
@@ -1147,9 +1071,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.signup.password.title',
|
||||
),
|
||||
tr('msg.userfront.signup.password.title'),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -1183,9 +1105,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
obscureText: true,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.password.label',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.password.label'),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _passwordError,
|
||||
),
|
||||
@@ -1211,16 +1131,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
if (requiresUpper)
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.uppercase',
|
||||
),
|
||||
tr('msg.userfront.signup.password.rule.uppercase'),
|
||||
hasUpper,
|
||||
),
|
||||
if (requiresLower)
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.lowercase',
|
||||
),
|
||||
tr('msg.userfront.signup.password.rule.lowercase'),
|
||||
hasLower,
|
||||
),
|
||||
if (requiresNumber)
|
||||
@@ -1230,9 +1146,7 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
),
|
||||
if (requiresSymbol)
|
||||
_cryptoCheck(
|
||||
tr(
|
||||
'msg.userfront.signup.password.rule.symbol',
|
||||
),
|
||||
tr('msg.userfront.signup.password.rule.symbol'),
|
||||
hasSpecial,
|
||||
),
|
||||
],
|
||||
@@ -1244,16 +1158,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_confirmPasswordError = (val != _passwordController.text)
|
||||
? tr(
|
||||
'msg.userfront.signup.password.mismatch',
|
||||
)
|
||||
? tr('msg.userfront.signup.password.mismatch')
|
||||
: null;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.signup.password.confirm_label',
|
||||
),
|
||||
labelText: tr('ui.userfront.signup.password.confirm_label'),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _confirmPasswordError,
|
||||
),
|
||||
@@ -1379,12 +1289,8 @@ class _SignupScreenState extends State<SignupScreen> {
|
||||
)
|
||||
: Text(
|
||||
_currentStep < 4
|
||||
? tr(
|
||||
'ui.userfront.signup.next_step',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.signup.complete',
|
||||
),
|
||||
? tr('ui.userfront.signup.next_step')
|
||||
: tr('ui.userfront.signup.complete'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -172,9 +172,7 @@ class AuthTimelineNotifier extends Notifier<AuthTimelineState> {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
isLoadingMore: false,
|
||||
error: tr(
|
||||
'msg.userfront.dashboard.timeline.load_error',
|
||||
),
|
||||
error: tr('msg.userfront.dashboard.timeline.load_error'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +71,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
tr('ui.userfront.dashboard.revoke.title'),
|
||||
),
|
||||
title: Text(tr('ui.userfront.dashboard.revoke.title')),
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.revoke.confirm',
|
||||
@@ -88,11 +86,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.revoke.confirm_button',
|
||||
),
|
||||
),
|
||||
child: Text(tr('ui.userfront.dashboard.revoke.confirm_button')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -166,17 +160,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.scopes.title',
|
||||
),
|
||||
tr('ui.userfront.dashboard.scopes.title'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (item.scopes.isEmpty)
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.scopes.empty',
|
||||
),
|
||||
tr('msg.userfront.dashboard.scopes.empty'),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
)
|
||||
else
|
||||
@@ -199,9 +189,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
tr(
|
||||
'ui.userfront.dashboard.status_history',
|
||||
),
|
||||
tr('ui.userfront.dashboard.status_history'),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -219,9 +207,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
builder: (context) {
|
||||
final statusLabel = item.status == 'active'
|
||||
? tr('ui.common.status.active')
|
||||
: tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
);
|
||||
: tr('ui.userfront.dashboard.status.revoked');
|
||||
return Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.current_status',
|
||||
@@ -534,12 +520,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
? log.detailMap['approved_session_id'].toString()
|
||||
: log.sessionId;
|
||||
final tooltipLabel = isOidc
|
||||
? tr(
|
||||
'ui.userfront.dashboard.approved_session.userfront',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.approved_session.default',
|
||||
);
|
||||
? tr('ui.userfront.dashboard.approved_session.userfront')
|
||||
: tr('ui.userfront.dashboard.approved_session.default');
|
||||
final tooltip = approvedSessionId.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.dashboard.approved_session.none',
|
||||
@@ -558,9 +540,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.session_id_copied',
|
||||
),
|
||||
tr('msg.userfront.dashboard.session_id_copied'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -628,12 +608,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
? log.detailMap['approved_session_id'].toString()
|
||||
: log.sessionId;
|
||||
final tooltipLabel = isOidc
|
||||
? tr(
|
||||
'ui.userfront.dashboard.approved_session.userfront',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.approved_session.default',
|
||||
);
|
||||
? tr('ui.userfront.dashboard.approved_session.userfront')
|
||||
: tr('ui.userfront.dashboard.approved_session.default');
|
||||
return InkWell(
|
||||
onTap: approvedSessionId.isEmpty
|
||||
? null
|
||||
@@ -643,9 +619,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.session_id_copied',
|
||||
),
|
||||
tr('msg.userfront.dashboard.session_id_copied'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -692,9 +666,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final label = _appLabelForLog(log);
|
||||
final clientId = log.clientId;
|
||||
final tooltip = clientId.isEmpty
|
||||
? tr(
|
||||
'msg.userfront.dashboard.client_id_missing',
|
||||
)
|
||||
? tr('msg.userfront.dashboard.client_id_missing')
|
||||
: tr(
|
||||
'msg.userfront.dashboard.client_id',
|
||||
fallback: 'Client ID: {{id}}',
|
||||
@@ -814,21 +786,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
const SizedBox(height: 28),
|
||||
],
|
||||
_buildSectionTitle(
|
||||
tr(
|
||||
'ui.userfront.sections.apps',
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.sections.apps_subtitle',
|
||||
),
|
||||
tr('ui.userfront.sections.apps'),
|
||||
tr('msg.userfront.sections.apps_subtitle'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActivitySection(isMobile),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.sections.audit'),
|
||||
tr(
|
||||
'msg.userfront.sections.audit_subtitle',
|
||||
),
|
||||
tr('msg.userfront.sections.audit_subtitle'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildAccessHistory(timelineState, timelineWide),
|
||||
@@ -857,10 +823,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.greeting',
|
||||
params: {'name': userName},
|
||||
),
|
||||
tr('msg.userfront.greeting', params: {'name': userName}),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -963,9 +926,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.empty',
|
||||
),
|
||||
tr('msg.userfront.dashboard.activities.empty'),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
@@ -974,9 +935,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.empty_detail',
|
||||
),
|
||||
tr('msg.userfront.dashboard.activities.empty_detail'),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
@@ -992,9 +951,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.activities.error',
|
||||
),
|
||||
tr('msg.userfront.dashboard.activities.error'),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -1194,9 +1151,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Text(
|
||||
item.status == 'active'
|
||||
? tr('ui.common.status.active')
|
||||
: tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
),
|
||||
: tr('ui.userfront.dashboard.status.revoked'),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: statusColor,
|
||||
@@ -1264,12 +1219,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
)
|
||||
: Text(
|
||||
item.isRevoked
|
||||
? tr(
|
||||
'ui.userfront.dashboard.status.revoked',
|
||||
)
|
||||
: tr(
|
||||
'ui.userfront.dashboard.revoke.title',
|
||||
),
|
||||
? tr('ui.userfront.dashboard.status.revoked')
|
||||
: tr('ui.userfront.dashboard.revoke.title'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
@@ -1303,22 +1254,14 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
}
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.link_open_error',
|
||||
),
|
||||
),
|
||||
content: Text(tr('msg.userfront.dashboard.link_open_error')),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.link_missing',
|
||||
),
|
||||
),
|
||||
content: Text(tr('msg.userfront.dashboard.link_missing')),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1344,11 +1287,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.audit_load_error',
|
||||
),
|
||||
),
|
||||
Text(tr('msg.userfront.dashboard.audit_load_error')),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
@@ -1365,9 +1304,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return _buildHistoryContainer(
|
||||
child: Center(
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.dashboard.audit_empty',
|
||||
),
|
||||
tr('msg.userfront.dashboard.audit_empty'),
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
),
|
||||
@@ -1416,16 +1353,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.date'),
|
||||
),
|
||||
label: Text(tr('ui.userfront.audit.table.date')),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.app',
|
||||
),
|
||||
),
|
||||
label: Text(tr('ui.userfront.audit.table.app')),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
@@ -1433,30 +1364,16 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.device',
|
||||
),
|
||||
),
|
||||
label: Text(tr('ui.userfront.audit.table.device')),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.auth_method',
|
||||
),
|
||||
),
|
||||
label: Text(tr('ui.userfront.audit.table.auth_method')),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr(
|
||||
'ui.userfront.audit.table.result',
|
||||
),
|
||||
),
|
||||
label: Text(tr('ui.userfront.audit.table.result')),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
tr('ui.userfront.audit.table.status'),
|
||||
),
|
||||
label: Text(tr('ui.userfront.audit.table.status')),
|
||||
),
|
||||
],
|
||||
rows: state.items.map((log) {
|
||||
@@ -1505,9 +1422,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
),
|
||||
DataCell(
|
||||
_selectableText(
|
||||
tr(
|
||||
'ui.userfront.audit.table.pending',
|
||||
),
|
||||
tr('ui.userfront.audit.table.pending'),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
@@ -1643,11 +1558,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.audit.load_more_error',
|
||||
),
|
||||
),
|
||||
Text(tr('msg.userfront.audit.load_more_error')),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
ref.read(authTimelineProvider.notifier).loadMore(),
|
||||
|
||||
@@ -25,9 +25,7 @@ class ProfileRepository {
|
||||
final token = await _getToken();
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
throw Exception(tr('err.userfront.session.missing'));
|
||||
}
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||
@@ -59,9 +57,7 @@ class ProfileRepository {
|
||||
final token = await _getToken();
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
throw Exception(tr('err.userfront.session.missing'));
|
||||
}
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me');
|
||||
@@ -95,9 +91,7 @@ class ProfileRepository {
|
||||
final token = await _getToken();
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
throw Exception(tr('err.userfront.session.missing'));
|
||||
}
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/send-code');
|
||||
@@ -130,9 +124,7 @@ class ProfileRepository {
|
||||
final token = await _getToken();
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
throw Exception(tr('err.userfront.session.missing'));
|
||||
}
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/password');
|
||||
@@ -165,9 +157,7 @@ class ProfileRepository {
|
||||
final token = await _getToken();
|
||||
final useCookie = AuthTokenStore.usesCookie();
|
||||
if (token == null && !useCookie) {
|
||||
throw Exception(
|
||||
tr('err.userfront.session.missing'),
|
||||
);
|
||||
throw Exception(tr('err.userfront.session.missing'));
|
||||
}
|
||||
|
||||
final url = Uri.parse('$_baseUrl/api/v1/user/me/verify-code');
|
||||
|
||||
@@ -232,13 +232,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.code_sent',
|
||||
),
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text(tr('msg.userfront.profile.phone.code_sent'))),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -272,11 +266,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr('msg.userfront.profile.phone.verified'),
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text(tr('msg.userfront.profile.phone.verified'))),
|
||||
);
|
||||
}
|
||||
if (_editingField == 'phone') {
|
||||
@@ -315,17 +305,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
}
|
||||
if (newPassword.isEmpty) {
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.new_required',
|
||||
),
|
||||
() =>
|
||||
_passwordError = tr('msg.userfront.profile.password.new_required'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (newPassword != confirmPassword) {
|
||||
setState(
|
||||
() => _passwordError = tr(
|
||||
'msg.userfront.profile.password.mismatch',
|
||||
),
|
||||
() => _passwordError = tr('msg.userfront.profile.password.mismatch'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -347,9 +334,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_newPasswordController?.clear();
|
||||
_confirmPasswordController?.clear();
|
||||
setState(() {
|
||||
_passwordSuccess = tr(
|
||||
'msg.userfront.profile.password.changed',
|
||||
);
|
||||
_passwordSuccess = tr('msg.userfront.profile.password.changed');
|
||||
});
|
||||
} catch (e) {
|
||||
final message = e.toString().replaceFirst('Exception: ', '');
|
||||
@@ -431,22 +416,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
|
||||
if (_editingField == 'name' && nextName.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr('msg.userfront.profile.name_required'),
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text(tr('msg.userfront.profile.name_required'))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_editingField == 'department' && nextDepartment.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.department_required',
|
||||
),
|
||||
),
|
||||
content: Text(tr('msg.userfront.profile.department_required')),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -454,24 +431,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
if (_editingField == 'phone') {
|
||||
if (nextPhone.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone_required',
|
||||
),
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text(tr('msg.userfront.profile.phone_required'))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_isPhoneChanged && !_isPhoneVerified) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone_verify_required',
|
||||
),
|
||||
),
|
||||
content: Text(tr('msg.userfront.profile.phone_verify_required')),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -511,13 +478,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
_departmentTouched = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.update_success',
|
||||
),
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text(tr('msg.userfront.profile.update_success'))),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -657,10 +618,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.greeting',
|
||||
params: {'name': name},
|
||||
),
|
||||
tr('msg.userfront.profile.greeting', params: {'name': name}),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -833,9 +791,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
child: Text(
|
||||
_isCodeSent
|
||||
? tr('ui.common.resend')
|
||||
: tr(
|
||||
'ui.userfront.profile.phone.request_code',
|
||||
),
|
||||
: tr('ui.userfront.profile.phone.request_code'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -859,9 +815,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
onSubmitted: (_) => _verifyCode(profile),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: tr(
|
||||
'ui.userfront.profile.phone.code_hint',
|
||||
),
|
||||
hintText: tr('ui.userfront.profile.phone.code_hint'),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -877,9 +831,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
tr(
|
||||
'msg.userfront.profile.phone.verify_notice',
|
||||
),
|
||||
tr('msg.userfront.profile.phone.verify_notice'),
|
||||
style: const TextStyle(color: Colors.orange, fontSize: 12),
|
||||
),
|
||||
),
|
||||
@@ -898,9 +850,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.password.subtitle',
|
||||
),
|
||||
tr('msg.userfront.profile.password.subtitle'),
|
||||
style: const TextStyle(color: Color(0xFF6B7280)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -908,9 +858,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
controller: _currentPasswordController,
|
||||
obscureText: !_showCurrentPassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.current',
|
||||
),
|
||||
labelText: tr('ui.userfront.profile.password.current'),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
@@ -929,9 +877,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
controller: _newPasswordController,
|
||||
obscureText: !_showNewPassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.new',
|
||||
),
|
||||
labelText: tr('ui.userfront.profile.password.new'),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
@@ -948,9 +894,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
controller: _confirmPasswordController,
|
||||
obscureText: !_showConfirmPassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr(
|
||||
'ui.userfront.profile.password.confirm',
|
||||
),
|
||||
labelText: tr('ui.userfront.profile.password.confirm'),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
@@ -986,20 +930,12 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(
|
||||
tr(
|
||||
'ui.userfront.profile.password.change',
|
||||
),
|
||||
),
|
||||
: Text(tr('ui.userfront.profile.password.change')),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
TextButton(
|
||||
onPressed: () => context.go('/recovery'),
|
||||
child: Text(
|
||||
tr(
|
||||
'ui.userfront.profile.password.forgot',
|
||||
),
|
||||
),
|
||||
child: Text(tr('ui.userfront.profile.password.forgot')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1024,9 +960,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.profile.section.basic'),
|
||||
tr(
|
||||
'msg.userfront.profile.section.basic',
|
||||
),
|
||||
tr('msg.userfront.profile.section.basic'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCard(
|
||||
@@ -1034,9 +968,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'name',
|
||||
label: tr(
|
||||
'ui.userfront.profile.field.name',
|
||||
),
|
||||
label: tr('ui.userfront.profile.field.name'),
|
||||
value: profile.name,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
@@ -1044,9 +976,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.email',
|
||||
),
|
||||
tr('ui.userfront.profile.field.email'),
|
||||
profile.email,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
@@ -1056,12 +986,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr(
|
||||
'ui.userfront.profile.section.organization',
|
||||
),
|
||||
tr(
|
||||
'msg.userfront.profile.section.organization',
|
||||
),
|
||||
tr('ui.userfront.profile.section.organization'),
|
||||
tr('msg.userfront.profile.section.organization'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCard(
|
||||
@@ -1069,9 +995,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
children: [
|
||||
_buildEditableTile(
|
||||
field: 'department',
|
||||
label: tr(
|
||||
'ui.userfront.profile.field.department',
|
||||
),
|
||||
label: tr('ui.userfront.profile.field.department'),
|
||||
value: profile.department,
|
||||
profile: profile,
|
||||
isUpdating: isUpdating,
|
||||
@@ -1079,26 +1003,20 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.affiliation',
|
||||
),
|
||||
tr('ui.userfront.profile.field.affiliation'),
|
||||
profile.affiliationType,
|
||||
),
|
||||
if (profile.tenant != null) ...[
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.tenant',
|
||||
),
|
||||
tr('ui.userfront.profile.field.tenant'),
|
||||
profile.tenant!.name,
|
||||
),
|
||||
],
|
||||
if (profile.companyCode.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
_buildReadOnlyTile(
|
||||
tr(
|
||||
'ui.userfront.profile.field.company_code',
|
||||
),
|
||||
tr('ui.userfront.profile.field.company_code'),
|
||||
profile.companyCode,
|
||||
),
|
||||
],
|
||||
@@ -1108,9 +1026,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
const SizedBox(height: 28),
|
||||
_buildSectionTitle(
|
||||
tr('ui.userfront.profile.section.security'),
|
||||
tr(
|
||||
'msg.userfront.profile.section.security',
|
||||
),
|
||||
tr('msg.userfront.profile.section.security'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildPasswordSection(),
|
||||
@@ -1137,20 +1053,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
||||
final profile = profileState.value ?? _cachedProfile;
|
||||
if (profile == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr('ui.userfront.nav.profile')),
|
||||
),
|
||||
appBar: AppBar(title: Text(tr('ui.userfront.nav.profile'))),
|
||||
body: profileState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
tr(
|
||||
'msg.userfront.profile.load_failed',
|
||||
),
|
||||
),
|
||||
Text(tr('msg.userfront.profile.load_failed')),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -131,9 +131,11 @@ final _router = GoRouter(
|
||||
GoRoute(
|
||||
path: 'signin',
|
||||
builder: (context, state) {
|
||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
||||
final redirectUrl = state.uri.queryParameters['redirect_uri'] ??
|
||||
state.uri.queryParameters['redirect_url'];
|
||||
final loginChallenge =
|
||||
state.uri.queryParameters['login_challenge'];
|
||||
final redirectUrl =
|
||||
state.uri.queryParameters['redirect_uri'] ??
|
||||
state.uri.queryParameters['redirect_url'];
|
||||
return LoginScreen(
|
||||
key: state.pageKey,
|
||||
loginChallenge: loginChallenge,
|
||||
@@ -145,9 +147,11 @@ final _router = GoRouter(
|
||||
path: 'login',
|
||||
builder: (context, state) {
|
||||
// IMPORTANT: Match signin logic to handle OIDC challenges
|
||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
||||
final redirectUrl = state.uri.queryParameters['redirect_uri'] ??
|
||||
state.uri.queryParameters['redirect_url'];
|
||||
final loginChallenge =
|
||||
state.uri.queryParameters['login_challenge'];
|
||||
final redirectUrl =
|
||||
state.uri.queryParameters['redirect_uri'] ??
|
||||
state.uri.queryParameters['redirect_url'];
|
||||
return LoginScreen(
|
||||
key: state.pageKey,
|
||||
loginChallenge: loginChallenge,
|
||||
@@ -158,10 +162,13 @@ final _router = GoRouter(
|
||||
GoRoute(
|
||||
path: 'consent',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
final consentChallenge = state.uri.queryParameters['consent_challenge'];
|
||||
final consentChallenge =
|
||||
state.uri.queryParameters['consent_challenge'];
|
||||
if (consentChallenge == null) {
|
||||
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);
|
||||
@@ -231,15 +238,13 @@ final _router = GoRouter(
|
||||
),
|
||||
GoRoute(
|
||||
path: 'approve',
|
||||
builder: (context, state) => ApproveQrScreen(
|
||||
pendingRef: state.uri.queryParameters['ref'],
|
||||
),
|
||||
builder: (context, state) =>
|
||||
ApproveQrScreen(pendingRef: state.uri.queryParameters['ref']),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'ql/:ref',
|
||||
builder: (context, state) => ApproveQrScreen(
|
||||
pendingRef: state.pathParameters['ref'],
|
||||
),
|
||||
builder: (context, state) =>
|
||||
ApproveQrScreen(pendingRef: state.pathParameters['ref']),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'scan',
|
||||
@@ -258,14 +263,15 @@ final _router = GoRouter(
|
||||
final uri = state.uri;
|
||||
final requestedLocale = extractLocaleFromPath(uri);
|
||||
final preferredLocale = resolvePreferredLocaleCode();
|
||||
|
||||
|
||||
if (requestedLocale == null) {
|
||||
final localizedPath = buildLocalizedPath(preferredLocale, uri);
|
||||
return localizedPath;
|
||||
}
|
||||
|
||||
final token = AuthTokenStore.getToken();
|
||||
final isLoggedIn = (token != null && token.isNotEmpty) || AuthTokenStore.usesCookie();
|
||||
final isLoggedIn =
|
||||
(token != null && token.isNotEmpty) || AuthTokenStore.usesCookie();
|
||||
final path = stripLocalePath(uri);
|
||||
|
||||
// Precise public path detection
|
||||
|
||||
Reference in New Issue
Block a user