fix: update translations and clean up imports in various components

- Translated SMTP connection test messages in French and Polish for better localization.
- Removed unused icon imports in the two-factor verification and profile components to streamline the code.
- Simplified user data extraction in the login hook for clarity and consistency.
This commit is contained in:
Daniel Luiz Alves
2025-07-08 00:40:26 -03:00
parent 7f76d48314
commit e4bdfb8432
19 changed files with 75 additions and 152 deletions

View File

@@ -15,7 +15,6 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }>
const MDXContent = page.data.body;
// Check if this is an older version page that needs a warning
const shouldShowWarning = page.url.startsWith("/docs/2.0.0-beta");
return (

View File

@@ -18,7 +18,6 @@ import {
TokenResponse,
} from "./types";
// Constants
const DEFAULT_BASE_URL = "http://localhost:3000";
const STATE_EXPIRY_TIME = 600000; // 10 minutes
const CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
@@ -43,7 +42,6 @@ export class AuthProvidersService {
setInterval(() => this.cleanupExpiredStates(), CLEANUP_INTERVAL);
}
// Utility methods
private buildBaseUrl(requestContext?: RequestContextService): string {
return requestContext ? `${requestContext.protocol}://${requestContext.host}` : DEFAULT_BASE_URL;
}
@@ -87,7 +85,6 @@ export class AuthProvidersService {
}
}
// Provider configuration methods
private isOfficial(providerName: string): boolean {
return providerName in providersConfig.officialProviders;
}
@@ -114,7 +111,6 @@ export class AuthProvidersService {
}
private async resolveEndpoints(provider: any, config: ProviderConfig): Promise<ProviderEndpoints> {
// Use custom endpoints if all are provided
if (provider.authorizationEndpoint && provider.tokenEndpoint && provider.userInfoEndpoint) {
return {
authorizationEndpoint: this.resolveEndpointUrl(provider.authorizationEndpoint, provider.issuerUrl),
@@ -123,7 +119,6 @@ export class AuthProvidersService {
};
}
// Try discovery if supported
if (config.supportsDiscovery && provider.issuerUrl) {
const discoveredEndpoints = await this.attemptDiscovery(provider.issuerUrl);
if (discoveredEndpoints) {
@@ -131,7 +126,6 @@ export class AuthProvidersService {
}
}
// Fallback to intelligent endpoints
const baseUrl = provider.issuerUrl?.replace(/\/$/, "") || "";
const detectedType = detectProviderType(provider.issuerUrl || "");
const fallbackPattern = getFallbackEndpoints(detectedType);
@@ -224,7 +218,6 @@ export class AuthProvidersService {
return config.specialHandling?.emailEndpoint || null;
}
// PKCE and OAuth setup methods
private setupPkceIfNeeded(provider: any): { codeVerifier?: string; codeChallenge?: string } {
const needsPkce = provider.type === DEFAULT_PROVIDER_TYPE;
@@ -263,7 +256,6 @@ export class AuthProvidersService {
return authUrl.toString();
}
// Callback handling methods
private validateAndGetPendingState(state: string): PendingState {
const pendingState = this.pendingStates.get(state);
@@ -299,7 +291,6 @@ export class AuthProvidersService {
};
}
// Public methods
async getEnabledProviders(requestContext?: RequestContextService) {
const providers = await prisma.authProvider.findMany({
where: { enabled: true },
@@ -605,16 +596,13 @@ export class AuthProvidersService {
throw new Error(ERROR_MESSAGES.MISSING_USER_INFO);
}
// First, check if there's already an auth provider entry for this external ID
const existingAuthProvider = await this.findExistingAuthProvider(provider.id, String(externalId));
if (existingAuthProvider) {
return await this.updateExistingUserFromProvider(existingAuthProvider.user, userInfo);
}
// Check if there's a user with this email
const existingUser = await this.findExistingUserByEmail(userInfo.email);
if (existingUser) {
// Check if this user already has this provider linked
const existingUserProvider = await prisma.userAuthProvider.findFirst({
where: {
userId: existingUser.id,

View File

@@ -72,10 +72,8 @@ export class EmailService {
let smtpConfig: SmtpConfig;
if (config) {
// Use provided configuration
smtpConfig = config;
} else {
// Fallback to saved configuration
smtpConfig = {
smtpEnabled: await this.configService.getValue("smtpEnabled"),
smtpHost: await this.configService.getValue("smtpHost"),

View File

@@ -533,7 +533,6 @@ export class ReverseShareService {
const { FilesystemStorageProvider } = await import("../../providers/filesystem-storage.provider.js");
const provider = FilesystemStorageProvider.getInstance();
// Use streaming copy for filesystem mode
const sourcePath = provider.getFilePath(file.objectName);
const fs = await import("fs");
const { pipeline } = await import("stream/promises");
@@ -541,14 +540,11 @@ export class ReverseShareService {
const sourceStream = fs.createReadStream(sourcePath);
const decryptStream = provider.createDecryptStream();
// Create a passthrough stream to get the decrypted content
const { PassThrough } = await import("stream");
const passThrough = new PassThrough();
// First, decrypt the source file into the passthrough stream
await pipeline(sourceStream, decryptStream, passThrough);
// Then upload the decrypted content
await provider.uploadFileFromStream(newObjectName, passThrough);
} else {
const downloadUrl = await this.fileService.getPresignedGetUrl(file.objectName, 300);

View File

@@ -31,14 +31,12 @@ export class TwoFactorService {
throw new Error("Two-factor authentication is already enabled");
}
// Generate secret
const secret = speakeasy.generateSecret({
name: `${appName || "Palmr"}:${userEmail}`,
issuer: appName || "Palmr",
length: 32,
});
// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url || "");
return {
@@ -70,7 +68,7 @@ export class TwoFactorService {
secret: secret,
encoding: "base32",
token: token,
window: 1, // Allow 1 step behind/ahead for clock drift
window: 1,
});
if (!verified) {

View File

@@ -179,7 +179,6 @@ export class FilesystemStorageProvider implements StorageProvider {
}
async uploadFile(objectName: string, buffer: Buffer): Promise<void> {
// For backward compatibility, convert buffer to stream and use streaming upload
const filePath = this.getFilePath(objectName);
const dir = path.dirname(filePath);
@@ -197,7 +196,6 @@ export class FilesystemStorageProvider implements StorageProvider {
await fs.mkdir(dir, { recursive: true });
// Use the new temp file system for better organization
const tempPath = getTempFilePath(objectName);
const tempDir = path.dirname(tempPath);
@@ -308,10 +306,8 @@ export class FilesystemStorageProvider implements StorageProvider {
*/
private async cleanupTempFile(tempPath: string): Promise<void> {
try {
// Remove the temp file
await fs.unlink(tempPath);
// Try to remove the parent directory if it's empty
const tempDir = path.dirname(tempPath);
try {
const files = await fs.readdir(tempDir);
@@ -319,7 +315,6 @@ export class FilesystemStorageProvider implements StorageProvider {
await fs.rmdir(tempDir);
}
} catch (dirError: any) {
// Ignore errors when trying to remove directory (might not be empty or might not exist)
if (dirError.code !== "ENOTEMPTY" && dirError.code !== "ENOENT") {
console.warn("Warning: Could not remove temp directory:", dirError.message);
}
@@ -338,11 +333,10 @@ export class FilesystemStorageProvider implements StorageProvider {
try {
const tempUploadsDir = directoriesConfig.tempUploads;
// Check if temp-uploads directory exists
try {
await fs.access(tempUploadsDir);
} catch {
return; // Directory doesn't exist, nothing to clean
return;
}
const items = await fs.readdir(tempUploadsDir);
@@ -354,14 +348,12 @@ export class FilesystemStorageProvider implements StorageProvider {
const stat = await fs.stat(itemPath);
if (stat.isDirectory()) {
// Check if directory is empty
const dirContents = await fs.readdir(itemPath);
if (dirContents.length === 0) {
await fs.rmdir(itemPath);
console.log(`🧹 Cleaned up empty temp directory: ${itemPath}`);
}
} else if (stat.isFile()) {
// Check if file is older than 1 hour (stale temp files)
const oneHourAgo = Date.now() - 60 * 60 * 1000;
if (stat.mtime.getTime() < oneHourAgo) {
await fs.unlink(itemPath);
@@ -369,7 +361,6 @@ export class FilesystemStorageProvider implements StorageProvider {
}
}
} catch (error: any) {
// Ignore errors for individual items
if (error.code !== "ENOENT") {
console.warn(`Warning: Could not process temp item ${itemPath}:`, error.message);
}

View File

@@ -64,7 +64,6 @@ export default [
"@typescript-eslint/no-var-requires": "off",
},
},
// Ignore ESLint errors in @/ui directory
{
ignores: ["src/components/ui/**/*"],
},

View File

@@ -1075,8 +1075,8 @@
"description": "URL de base du serveur Palmr (ex: https://palmr.exemple.com)"
},
"testSmtp": {
"title": "[TO_TRANSLATE] Test SMTP Connection",
"description": "[TO_TRANSLATE] Test if the SMTP configuration is valid"
"title": "Test de la Connexion SMTP",
"description": "Tester si la configuration SMTP est valide"
},
"smtpNoAuth": {
"title": "Pas d'Authentification",

View File

@@ -1152,7 +1152,7 @@
"description": "To udostępnienie mogło zostać usunięte lub wygasło."
},
"pageTitle": "Udostępnij",
"downloadAll": "[TO_TRANSLATE] Download All"
"downloadAll": "Pobierz wszystkie"
},
"shareActions": {
"deleteTitle": "Usuń udostępnienie",

View File

@@ -213,7 +213,6 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
return false;
}
// Check if either name or email is required
const nameRequired = reverseShare.nameFieldRequired === "REQUIRED";
const emailRequired = reverseShare.emailFieldRequired === "REQUIRED";
@@ -227,9 +226,6 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
return false;
}
// Remove the validation that requires at least one field when both are optional
// When both fields are OPTIONAL, they should be truly optional (can be empty)
return true;
};
@@ -275,8 +271,6 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
if (emailRequired && !uploaderEmail.trim()) return false;
// When both fields are OPTIONAL, they should be truly optional (can be empty)
// Remove the check that requires at least one field to be filled
return true;
};

View File

@@ -460,7 +460,6 @@ export function ReceivedFilesModal({
const { editingFile, editValue, setEditValue, inputRef, startEdit, cancelEdit } = useFileEdit();
// Clear selections when files change
useEffect(() => {
setSelectedFiles(new Set());
}, [reverseShare?.files]);
@@ -651,7 +650,6 @@ export function ReceivedFilesModal({
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Clear selections after successful download
setSelectedFiles(new Set());
})(),
{
@@ -684,7 +682,6 @@ export function ReceivedFilesModal({
await Promise.all(copyPromises);
// Clear selections after successful copy
setSelectedFiles(new Set());
} finally {
setBulkCopying(false);
@@ -732,7 +729,6 @@ export function ReceivedFilesModal({
await Promise.all(deletePromises);
// Clear selections and refresh data
setSelectedFiles(new Set());
setFilesToDeleteBulk([]);
if (onRefresh) {

View File

@@ -44,7 +44,6 @@ export default function AuthCallbackPage() {
}
if (token) {
// Set the token in a cookie and redirect to dashboard (using Palmr's standard cookie name)
document.cookie = `token=${token}; path=/; max-age=${7 * 24 * 60 * 60}; samesite=lax`;
toast.success("Successfully authenticated!");
@@ -52,7 +51,6 @@ export default function AuthCallbackPage() {
return;
}
// If no token or error, redirect to login
router.push("/login");
}, [router, searchParams]);

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { IconEye, IconEyeClosed, IconShield } from "@tabler/icons-react";
import { IconShield } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
@@ -25,7 +25,7 @@ export function TwoFactorVerification({
error,
isSubmitting,
}: TwoFactorVerificationProps) {
const t = useTranslations("twoFactor");
const t = useTranslations();
const [showBackupCode, setShowBackupCode] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
@@ -50,22 +50,22 @@ export function TwoFactorVerification({
<IconShield className="h-8 w-8 text-primary" />
</div>
</div>
<CardTitle>{t("verification.title")}</CardTitle>
<CardTitle>{t("twoFactor.verification.title")}</CardTitle>
<CardDescription>
{showBackupCode ? t("verification.backupDescription") : t("verification.description")}
{showBackupCode ? t("twoFactor.verification.backupDescription") : t("twoFactor.verification.description")}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="twoFactorCode" className="mb-2">
{showBackupCode ? t("verification.backupCode") : t("verification.verificationCode")}
{showBackupCode ? t("twoFactor.verification.backupCode") : t("twoFactor.verification.verificationCode")}
</Label>
{showBackupCode ? (
<Input
id="twoFactorCode"
type="text"
placeholder={t("verification.backupCodePlaceholder")}
placeholder={t("twoFactor.verification.backupCodePlaceholder")}
value={twoFactorCode}
onChange={handleBackupCodeChange}
className="text-center tracking-widest font-mono"
@@ -95,7 +95,7 @@ export function TwoFactorVerification({
className="w-full"
disabled={isSubmitting || twoFactorCode.length < (showBackupCode ? 8 : 6)}
>
{isSubmitting ? t("verification.verifying") : t("verification.verify")}
{isSubmitting ? t("twoFactor.verification.verifying") : t("twoFactor.verification.verify")}
</Button>
{error && (
@@ -112,7 +112,9 @@ export function TwoFactorVerification({
}}
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
{showBackupCode ? t("verification.useAuthenticatorCode") : t("verification.useBackupCode")}
{showBackupCode
? t("twoFactor.verification.useAuthenticatorCode")
: t("twoFactor.verification.useBackupCode")}
</Button>
</div>
</form>

View File

@@ -150,8 +150,7 @@ export function useLogin() {
token: twoFactorCode,
});
const userResponse = await getCurrentUser();
const { isAdmin, ...userData } = userResponse.data.user;
const { isAdmin, ...userData } = response.data.user;
setUser(userData);
setIsAdmin(isAdmin);

View File

@@ -9,7 +9,6 @@ import {
IconKey,
IconShield,
IconShieldCheck,
IconX,
} from "@tabler/icons-react";
import { useTranslations } from "next-intl";
@@ -29,7 +28,7 @@ import { Label } from "@/components/ui/label";
import { useTwoFactor } from "../hooks/use-two-factor";
export function TwoFactorForm() {
const t = useTranslations("twoFactor");
const t = useTranslations();
const {
isLoading,
status,
@@ -61,7 +60,7 @@ export function TwoFactorForm() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<IconShield className="h-5 w-5" />
{t("title")}
{t("twoFactor.title")}
</CardTitle>
<CardDescription>Loading...</CardDescription>
</CardHeader>
@@ -79,9 +78,9 @@ export function TwoFactorForm() {
) : (
<IconShield className="h-5 w-5" />
)}
{t("title")}
{t("twoFactor.title")}
</CardTitle>
<CardDescription>{status.enabled ? t("enabled") : t("description")}</CardDescription>
<CardDescription>{status.enabled ? t("twoFactor.enabled") : t("twoFactor.description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
@@ -89,7 +88,7 @@ export function TwoFactorForm() {
<p className="font-medium">Status: {status.enabled ? "Enabled" : "Disabled"}</p>
{status.enabled && (
<p className="text-sm text-muted-foreground">
{t("backupCodes.available", { count: status.availableBackupCodes })}
{t("twoFactor.backupCodes.available", { count: status.availableBackupCodes })}
</p>
)}
</div>
@@ -98,7 +97,7 @@ export function TwoFactorForm() {
<>
<Button variant="outline" onClick={generateNewBackupCodes} disabled={isLoading}>
<IconKey className="h-4 w-4" />
{t("backupCodes.generateNew")}
{t("twoFactor.backupCodes.generateNew")}
</Button>
<Button variant="destructive" onClick={() => setIsDisableModalOpen(true)} disabled={isLoading}>
Disable 2FA
@@ -119,8 +118,8 @@ export function TwoFactorForm() {
<Dialog open={isSetupModalOpen} onOpenChange={setIsSetupModalOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("setup.title")}</DialogTitle>
<DialogDescription>{t("setup.description")}</DialogDescription>
<DialogTitle>{t("twoFactor.setup.title")}</DialogTitle>
<DialogDescription>{t("twoFactor.setup.description")}</DialogDescription>
</DialogHeader>
{setupData && (
@@ -132,7 +131,7 @@ export function TwoFactorForm() {
{/* Manual Entry */}
<div>
<Label className="text-sm font-medium">{t("setup.manualEntryKey")}</Label>
<Label className="text-sm font-medium">{t("twoFactor.setup.manualEntryKey")}</Label>
<div className="flex items-center gap-2 mt-1">
<Input value={setupData.manualEntryKey} readOnly className="font-mono text-xs" />
<Button
@@ -148,7 +147,7 @@ export function TwoFactorForm() {
{/* Verification Code */}
<div>
<Label htmlFor="verification-code" className="mb-2">
{t("setup.verificationCode")}
{t("twoFactor.setup.verificationCode")}
</Label>
<div className="flex justify-start">
<InputOTP maxLength={6} value={verificationCode} onChange={setVerificationCode}>
@@ -162,17 +161,19 @@ export function TwoFactorForm() {
</InputOTPGroup>
</InputOTP>
</div>
<span className="text-sm text-muted-foreground mt-1">{t("setup.verificationCodeDescription")}</span>
<span className="text-sm text-muted-foreground mt-1">
{t("twoFactor.setup.verificationCodeDescription")}
</span>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setIsSetupModalOpen(false)} disabled={isLoading}>
{t("setup.cancel")}
{t("twoFactor.setup.cancel")}
</Button>
<Button onClick={verifySetup} disabled={isLoading || !verificationCode || verificationCode.length !== 6}>
{t("setup.verifyAndEnable")}
{t("twoFactor.setup.verifyAndEnable")}
</Button>
</DialogFooter>
</DialogContent>
@@ -182,20 +183,20 @@ export function TwoFactorForm() {
<Dialog open={isDisableModalOpen} onOpenChange={setIsDisableModalOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("disable.title")}</DialogTitle>
<DialogDescription>{t("disable.description")}</DialogDescription>
<DialogTitle>{t("twoFactor.disable.title")}</DialogTitle>
<DialogDescription>{t("twoFactor.disable.description")}</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="disable-password" className="mb-2">
{t("disable.password")}
{t("twoFactor.disable.password")}
</Label>
<div className="relative">
<Input
id="disable-password"
type={showPassword ? "text" : "password"}
placeholder={t("disable.passwordPlaceholder")}
placeholder={t("twoFactor.disable.passwordPlaceholder")}
value={disablePassword}
onChange={(e) => setDisablePassword(e.target.value)}
/>
@@ -218,10 +219,10 @@ export function TwoFactorForm() {
<DialogFooter>
<Button variant="outline" onClick={() => setIsDisableModalOpen(false)} disabled={isLoading}>
{t("disable.cancel")}
{t("twoFactor.disable.cancel")}
</Button>
<Button variant="destructive" onClick={disable2FA} disabled={isLoading || !disablePassword}>
{t("disable.confirm")}
{t("twoFactor.disable.confirm")}
</Button>
</DialogFooter>
</DialogContent>
@@ -231,8 +232,8 @@ export function TwoFactorForm() {
<Dialog open={isBackupCodesModalOpen} onOpenChange={setIsBackupCodesModalOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("backupCodes.title")}</DialogTitle>
<DialogDescription>{t("backupCodes.description")}</DialogDescription>
<DialogTitle>{t("twoFactor.backupCodes.title")}</DialogTitle>
<DialogDescription>{t("twoFactor.backupCodes.description")}</DialogDescription>
</DialogHeader>
<div className="space-y-4">
@@ -249,11 +250,11 @@ export function TwoFactorForm() {
<div className="flex gap-2">
<Button variant="outline" onClick={downloadBackupCodes} className="flex-1">
<IconDownload className="h-4 w-4" />
{t("backupCodes.download")}
{t("twoFactor.backupCodes.download")}
</Button>
<Button variant="outline" onClick={copyBackupCodes} className="flex-1">
<IconCopy className="h-4 w-4" />
{t("backupCodes.copyToClipboard")}
{t("twoFactor.backupCodes.copyToClipboard")}
</Button>
</div>
@@ -265,7 +266,7 @@ export function TwoFactorForm() {
</div>
<DialogFooter>
<Button onClick={() => setIsBackupCodesModalOpen(false)}>{t("backupCodes.savedMessage")}</Button>
<Button onClick={() => setIsBackupCodesModalOpen(false)}>{t("twoFactor.backupCodes.savedMessage")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -12,14 +12,10 @@ import {
getTwoFactorStatus,
verifyTwoFactorSetup,
} from "@/http/endpoints/auth/two-factor";
import type {
TwoFactorSetupResponse,
TwoFactorStatus,
VerifySetupResponse,
} from "@/http/endpoints/auth/two-factor/types";
import type { TwoFactorSetupResponse, TwoFactorStatus } from "@/http/endpoints/auth/two-factor/types";
export function useTwoFactor() {
const t = useTranslations("twoFactor");
const t = useTranslations();
const { appName } = useAppInfo();
const [isLoading, setIsLoading] = useState(true);
const [status, setStatus] = useState<TwoFactorStatus>({
@@ -35,7 +31,6 @@ export function useTwoFactor() {
const [verificationCode, setVerificationCode] = useState("");
const [disablePassword, setDisablePassword] = useState("");
// Load 2FA status
const loadStatus = useCallback(async () => {
try {
setIsLoading(true);
@@ -43,13 +38,12 @@ export function useTwoFactor() {
setStatus(response.data);
} catch (error) {
console.error("Failed to load 2FA status:", error);
toast.error(t("messages.statusLoadFailed"));
toast.error(t("twoFactor.messages.statusLoadFailed"));
} finally {
setIsLoading(false);
}
}, []);
}, [t]);
// Generate 2FA setup
const startSetup = async () => {
try {
setIsLoading(true);
@@ -61,17 +55,16 @@ export function useTwoFactor() {
if (error.response?.data?.error) {
toast.error(error.response.data.error);
} else {
toast.error(t("messages.setupFailed"));
toast.error(t("twoFactor.messages.setupFailed"));
}
} finally {
setIsLoading(false);
}
};
// Verify setup and enable 2FA
const verifySetup = async () => {
if (!setupData || !verificationCode) {
toast.error(t("messages.enterVerificationCode"));
toast.error(t("twoFactor.messages.enterVerificationCode"));
return;
}
@@ -87,7 +80,7 @@ export function useTwoFactor() {
setIsSetupModalOpen(false);
setIsBackupCodesModalOpen(true);
setVerificationCode("");
toast.success(t("messages.enabledSuccess"));
toast.success(t("twoFactor.messages.enabledSuccess"));
await loadStatus();
}
} catch (error: any) {
@@ -95,17 +88,16 @@ export function useTwoFactor() {
if (error.response?.data?.error) {
toast.error(error.response.data.error);
} else {
toast.error(t("messages.verificationFailed"));
toast.error(t("twoFactor.messages.verificationFailed"));
}
} finally {
setIsLoading(false);
}
};
// Disable 2FA
const disable2FA = async () => {
if (!disablePassword) {
toast.error(t("messages.enterPassword"));
toast.error(t("twoFactor.messages.enterPassword"));
return;
}
@@ -118,7 +110,7 @@ export function useTwoFactor() {
if (response.data.success) {
setIsDisableModalOpen(false);
setDisablePassword("");
toast.success(t("messages.disabledSuccess"));
toast.success(t("twoFactor.messages.disabledSuccess"));
await loadStatus();
}
} catch (error: any) {
@@ -126,7 +118,7 @@ export function useTwoFactor() {
if (error.response?.data?.error) {
toast.error(error.response.data.error);
} else {
toast.error(t("messages.disableFailed"));
toast.error(t("twoFactor.messages.disableFailed"));
}
} finally {
setIsLoading(false);
@@ -139,14 +131,14 @@ export function useTwoFactor() {
const response = await generateBackupCodes();
setBackupCodes(response.data.backupCodes);
setIsBackupCodesModalOpen(true);
toast.success(t("messages.backupCodesGenerated"));
toast.success(t("twoFactor.messages.backupCodesGenerated"));
await loadStatus();
} catch (error: any) {
console.error("Failed to generate backup codes:", error);
if (error.response?.data?.error) {
toast.error(error.response.data.error);
} else {
toast.error(t("messages.backupCodesFailed"));
toast.error(t("twoFactor.messages.backupCodesFailed"));
}
} finally {
setIsLoading(false);
@@ -169,9 +161,9 @@ export function useTwoFactor() {
const copyBackupCodes = async () => {
try {
await navigator.clipboard.writeText(backupCodes.join("\n"));
toast.success(t("messages.backupCodesCopied"));
} catch (error) {
toast.error(t("messages.backupCodesCopyFailed"));
toast.success(t("twoFactor.messages.backupCodesCopied"));
} catch {
toast.error(t("twoFactor.messages.backupCodesCopyFailed"));
}
};
@@ -180,7 +172,6 @@ export function useTwoFactor() {
}, [loadStatus]);
return {
// State
isLoading,
status,
setupData,
@@ -188,19 +179,16 @@ export function useTwoFactor() {
verificationCode,
disablePassword,
// Modal states
isSetupModalOpen,
isDisableModalOpen,
isBackupCodesModalOpen,
// Setters
setVerificationCode,
setDisablePassword,
setIsSetupModalOpen,
setIsDisableModalOpen,
setIsBackupCodesModalOpen,
// Actions
startSetup,
verifySetup,
disable2FA,

View File

@@ -70,7 +70,6 @@ export function EditProviderForm({
const [showClientSecret, setShowClientSecret] = useState(false);
const isOfficial = provider.isOfficial;
// Função para identificar providers oficiais que não devem ter o campo de provider URL editável
const isProviderUrlEditable = (providerName: string): boolean => {
const nonEditableProviders = ["google", "discord", "github"];
return !nonEditableProviders.includes(providerName.toLowerCase());

View File

@@ -77,7 +77,6 @@ export function SettingsInput({
);
}
// Use FileSizeInput for storage size fields
if (config.key === "maxFileSize" || config.key === "maxTotalStoragePerUser") {
const currentValue = watch(`configs.${config.key}`) || "0";
return (

View File

@@ -41,7 +41,6 @@ import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// Custom CSS for additional grid columns
const customStyles = `
.grid-cols-16 {
grid-template-columns: repeat(16, minmax(0, 1fr));
@@ -51,9 +50,8 @@ const customStyles = `
}
`;
// Inject custom styles
if (typeof document !== "undefined") {
const styleElement = document.createElement("style");
const styleElement = document.createElement("iconPicker.style");
styleElement.textContent = customStyles;
if (!document.head.querySelector("style[data-icon-picker]")) {
styleElement.setAttribute("data-icon-picker", "true");
@@ -73,11 +71,9 @@ interface IconPickerProps {
placeholder?: string;
}
// Lazy loading configuration
const ICONS_PER_BATCH = 100;
const SCROLL_THRESHOLD = 200; // pixels from bottom to trigger load
const SCROLL_THRESHOLD = 200;
// Virtualized Icon Grid Component with Lazy Loading
interface VirtualizedIconGridProps {
icons: IconData[];
onIconSelect: (iconName: string) => void;
@@ -86,13 +82,12 @@ interface VirtualizedIconGridProps {
}
function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories = false }: VirtualizedIconGridProps) {
const t = useTranslations("iconPicker");
const t = useTranslations();
const [visibleCount, setVisibleCount] = useState(ICONS_PER_BATCH);
const [isLoading, setIsLoading] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const sentinelRef = useRef<HTMLDivElement>(null);
// Intersection Observer for infinite scroll
useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel) return;
@@ -102,7 +97,6 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
const entry = entries[0];
if (entry.isIntersecting && visibleCount < icons.length && !isLoading) {
setIsLoading(true);
// Simulate async loading with setTimeout for better UX
setTimeout(() => {
setVisibleCount((prev) => Math.min(prev + ICONS_PER_BATCH, icons.length));
setIsLoading(false);
@@ -123,19 +117,16 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
};
}, [visibleCount, icons.length, isLoading]);
// Reset visible count when icons change
useEffect(() => {
setVisibleCount(ICONS_PER_BATCH);
}, [icons]);
// Reset visible count when switching between search and category view
useEffect(() => {
setVisibleCount(ICONS_PER_BATCH);
}, [showCategories]);
const visibleIcons = useMemo(() => icons.slice(0, visibleCount), [icons, visibleCount]);
// Group icons by category for category view - always compute, use conditionally
const iconsByCategory = useMemo(() => {
if (!showCategories) return [];
const grouped = new Map<string, IconData[]>();
@@ -155,7 +146,7 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
{iconsByCategory.map(([category, categoryIcons]) => (
<div key={category}>
<Badge variant="secondary" className="text-xs mb-3">
{t("categoryBadge", {
{t("iconPicker.categoryBadge", {
category,
count: icons.filter((icon) => icon.category === category).length,
})}
@@ -177,10 +168,10 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
{/* Loading indicator and sentinel */}
<div ref={sentinelRef} className="flex justify-center py-4">
{isLoading && <div className="text-sm text-muted-foreground">{t("loadingMore")}</div>}
{isLoading && <div className="text-sm text-muted-foreground">{t("iconPicker.loadingMore")}</div>}
{visibleCount >= icons.length && icons.length > 0 && (
<div className="text-sm text-muted-foreground">
{t("allIconsLoaded", { count: icons.length.toLocaleString() })}
{t("iconPicker.allIconsLoaded", { count: icons.length.toLocaleString() })}
</div>
)}
</div>
@@ -189,7 +180,6 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
);
}
// Simple grid view for search results and specific tabs
return (
<div ref={scrollRef} className="max-h-[600px] overflow-y-auto overflow-x-hidden pr-2">
<div className="grid grid-cols-8 sm:grid-cols-12 lg:grid-cols-16 xl:grid-cols-20 gap-2 sm:gap-3">
@@ -205,12 +195,11 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
))}
</div>
{/* Loading indicator and sentinel */}
<div ref={sentinelRef} className="flex justify-center py-4">
{isLoading && <div className="text-sm text-muted-foreground">{t("loadingMore")}</div>}
{isLoading && <div className="text-sm text-muted-foreground">{t("iconPicker.loadingMore")}</div>}
{visibleCount >= icons.length && icons.length > 0 && (
<div className="text-sm text-muted-foreground">
{t("allIconsLoaded", { count: icons.length.toLocaleString() })}
{t("iconPicker.allIconsLoaded", { count: icons.length.toLocaleString() })}
</div>
)}
</div>
@@ -218,7 +207,6 @@ function VirtualizedIconGrid({ icons, onIconSelect, renderIcon, showCategories =
);
}
// Popular icons for quick access
const POPULAR_ICONS = [
"FaGoogle",
"FaGithub",
@@ -252,7 +240,6 @@ const POPULAR_ICONS = [
"TbKey",
];
// Auth provider specific icons
const AUTH_PROVIDER_ICONS = [
"SiGoogle",
"SiGithub",
@@ -277,14 +264,12 @@ const AUTH_PROVIDER_ICONS = [
];
export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
const t = useTranslations("iconPicker");
const t = useTranslations();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
// Use translation for placeholder if not provided
const displayPlaceholder = placeholder || t("placeholder");
const displayPlaceholder = placeholder || t("iconPicker.placeholder");
// Combine ALL icon libraries
const allIcons = useMemo(() => {
const iconSets = [
{ icons: AiIcons, prefix: "Ai", category: "Ant Design Icons" },
@@ -344,23 +329,19 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
return icons;
}, []);
// Filter icons based on search
const filteredIcons = useMemo(() => {
if (!search) return allIcons;
return allIcons.filter((icon) => icon.name.toLowerCase().includes(search.toLowerCase()));
}, [allIcons, search]);
// Get popular icons
const popularIcons = useMemo(() => {
return POPULAR_ICONS.map((name) => allIcons.find((icon) => icon.name === name)).filter(Boolean) as IconData[];
}, [allIcons]);
// Get auth provider icons
const authProviderIcons = useMemo(() => {
return AUTH_PROVIDER_ICONS.map((name) => allIcons.find((icon) => icon.name === name)).filter(Boolean) as IconData[];
}, [allIcons]);
// Get current icon component
const currentIcon = useMemo(() => {
if (!value) return null;
return allIcons.find((icon) => icon.name === value);
@@ -380,7 +361,6 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
return <IconComponent className={className} size={size || 32} />;
}, []);
// Get unique categories for display
const categories = useMemo(() => {
const uniqueCategories = Array.from(new Set(allIcons.map((icon) => icon.category)));
return uniqueCategories.sort();
@@ -406,9 +386,9 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
<DialogContent className="max-w-5xl xl:max-w-6xl max-h-[90vh] overflow-hidden">
<div className="space-y-4 overflow-hidden">
<div className="flex items-center justify-between">
<DialogTitle>{t("title")}</DialogTitle>
<DialogTitle>{t("iconPicker.title")}</DialogTitle>
<div className="text-sm text-muted-foreground">
{t("stats", {
{t("iconPicker.stats", {
iconCount: allIcons.length.toLocaleString(),
libraryCount: categories.length,
})}
@@ -419,7 +399,7 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t("searchPlaceholder")}
placeholder={t("iconPicker.searchPlaceholder")}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-8"
@@ -438,9 +418,9 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
<Tabs defaultValue="all" className="w-full overflow-hidden">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="all">{t("tabs.all")}</TabsTrigger>
<TabsTrigger value="popular">{t("tabs.popular")}</TabsTrigger>
<TabsTrigger value="auth">{t("tabs.auth")}</TabsTrigger>
<TabsTrigger value="all">{t("iconPicker.tabs.all")}</TabsTrigger>
<TabsTrigger value="popular">{t("iconPicker.tabs.popular")}</TabsTrigger>
<TabsTrigger value="auth">{t("iconPicker.tabs.auth")}</TabsTrigger>
</TabsList>
{/* All Icons */}
@@ -490,7 +470,7 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
{search && filteredIcons.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<Search className="mx-auto h-12 w-12 opacity-50 mb-2" />
<p>{t("noIconsFound", { search })}</p>
<p>{t("iconPicker.noIconsFound", { search })}</p>
</div>
)}
</div>
@@ -499,7 +479,6 @@ export function IconPicker({ value, onChange, placeholder }: IconPickerProps) {
);
}
// Utility function to render an icon by name
export function renderIconByName(iconName: string, className = "w-5 h-5") {
const iconSets = [
AiIcons,
@@ -542,6 +521,5 @@ export function renderIconByName(iconName: string, className = "w-5 h-5") {
}
}
// Fallback to a default icon
return React.createElement(FaIcons.FaCog, { className });
}