Code cleanup for 1.7.0
This commit is contained in:
@@ -496,7 +496,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
process.env.NODE_ENV === "production"
|
||||
? path.join(process.env.DATA_DIR || "./db/data", ".temp", "exports")
|
||||
: path.join(os.tmpdir(), "termix-exports");
|
||||
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
@@ -861,7 +861,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||
|
||||
const fileStream = fs.createReadStream(tempPath);
|
||||
|
||||
|
||||
fileStream.on("error", (streamError) => {
|
||||
apiLogger.error("File stream error during export", streamError, {
|
||||
operation: "export_file_stream_error",
|
||||
@@ -882,13 +882,13 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
userId,
|
||||
filename,
|
||||
});
|
||||
|
||||
|
||||
fs.unlink(tempPath, (err) => {
|
||||
if (err) {
|
||||
apiLogger.warn("Failed to clean up export file", {
|
||||
apiLogger.warn("Failed to clean up export file", {
|
||||
operation: "export_cleanup_failed",
|
||||
path: tempPath,
|
||||
error: err.message
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -569,7 +569,9 @@ async function connectSSHTunnel(
|
||||
|
||||
if (tunnelConfig.endpointCredentialId && tunnelConfig.endpointUserId) {
|
||||
try {
|
||||
const userDataKey = DataCrypto.getUserDataKey(tunnelConfig.endpointUserId);
|
||||
const userDataKey = DataCrypto.getUserDataKey(
|
||||
tunnelConfig.endpointUserId,
|
||||
);
|
||||
if (userDataKey) {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
getDb()
|
||||
|
||||
@@ -56,10 +56,11 @@ export class DatabaseMigration {
|
||||
if (hasEncryptedDb && hasUnencryptedDb) {
|
||||
const unencryptedSize = fs.statSync(this.unencryptedDbPath).size;
|
||||
const encryptedSize = fs.statSync(this.encryptedDbPath).size;
|
||||
|
||||
|
||||
if (unencryptedSize === 0) {
|
||||
needsMigration = false;
|
||||
reason = "Empty unencrypted database found alongside encrypted database. Removing empty file.";
|
||||
reason =
|
||||
"Empty unencrypted database found alongside encrypted database. Removing empty file.";
|
||||
try {
|
||||
fs.unlinkSync(this.unencryptedDbPath);
|
||||
databaseLogger.info("Removed empty unencrypted database file", {
|
||||
|
||||
@@ -28,7 +28,7 @@ class SystemCrypto {
|
||||
|
||||
const dataDir = process.env.DATA_DIR || "./db/data";
|
||||
const envPath = path.join(dataDir, ".env");
|
||||
|
||||
|
||||
try {
|
||||
const envContent = await fs.readFile(envPath, "utf8");
|
||||
const jwtMatch = envContent.match(/^JWT_SECRET=(.+)$/m);
|
||||
@@ -37,8 +37,7 @@ class SystemCrypto {
|
||||
process.env.JWT_SECRET = jwtMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await this.generateAndGuideUser();
|
||||
} catch (error) {
|
||||
@@ -66,7 +65,7 @@ class SystemCrypto {
|
||||
|
||||
const dataDir = process.env.DATA_DIR || "./db/data";
|
||||
const envPath = path.join(dataDir, ".env");
|
||||
|
||||
|
||||
try {
|
||||
const envContent = await fs.readFile(envPath, "utf8");
|
||||
const dbKeyMatch = envContent.match(/^DATABASE_KEY=(.+)$/m);
|
||||
@@ -75,8 +74,7 @@ class SystemCrypto {
|
||||
process.env.DATABASE_KEY = dbKeyMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await this.generateAndGuideDatabaseKey();
|
||||
} catch (error) {
|
||||
@@ -104,7 +102,7 @@ class SystemCrypto {
|
||||
|
||||
const dataDir = process.env.DATA_DIR || "./db/data";
|
||||
const envPath = path.join(dataDir, ".env");
|
||||
|
||||
|
||||
try {
|
||||
const envContent = await fs.readFile(envPath, "utf8");
|
||||
const tokenMatch = envContent.match(/^INTERNAL_AUTH_TOKEN=(.+)$/m);
|
||||
@@ -113,8 +111,7 @@ class SystemCrypto {
|
||||
process.env.INTERNAL_AUTH_TOKEN = tokenMatch[1];
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await this.generateAndGuideInternalAuthToken();
|
||||
} catch (error) {
|
||||
|
||||
@@ -71,7 +71,7 @@ class UserCrypto {
|
||||
|
||||
async setupOIDCUserEncryption(userId: string): Promise<void> {
|
||||
const DEK = crypto.randomBytes(UserCrypto.DEK_LENGTH);
|
||||
|
||||
|
||||
const now = Date.now();
|
||||
this.userSessions.set(userId, {
|
||||
dataKey: Buffer.from(DEK),
|
||||
@@ -319,7 +319,8 @@ class UserCrypto {
|
||||
}
|
||||
|
||||
private deriveOIDCSystemKey(userId: string): Buffer {
|
||||
const systemSecret = process.env.OIDC_SYSTEM_SECRET || "termix-oidc-system-secret-default";
|
||||
const systemSecret =
|
||||
process.env.OIDC_SYSTEM_SECRET || "termix-oidc-system-secret-default";
|
||||
const salt = Buffer.from(userId, "utf8");
|
||||
return crypto.pbkdf2Sync(
|
||||
systemSecret,
|
||||
|
||||
@@ -271,7 +271,8 @@ export function AdminSettings({
|
||||
|
||||
setExportLoading(true);
|
||||
try {
|
||||
const isDev = process.env.NODE_ENV === "development" &&
|
||||
const isDev =
|
||||
process.env.NODE_ENV === "development" &&
|
||||
(window.location.port === "3000" ||
|
||||
window.location.port === "5173" ||
|
||||
window.location.port === "" ||
|
||||
@@ -340,7 +341,8 @@ export function AdminSettings({
|
||||
|
||||
setImportLoading(true);
|
||||
try {
|
||||
const isDev = process.env.NODE_ENV === "development" &&
|
||||
const isDev =
|
||||
process.env.NODE_ENV === "development" &&
|
||||
(window.location.port === "3000" ||
|
||||
window.location.port === "5173" ||
|
||||
window.location.port === "" ||
|
||||
|
||||
@@ -87,7 +87,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
initialHost || null,
|
||||
);
|
||||
const [currentPath, setCurrentPath] = useState(
|
||||
initialHost?.defaultPath || "/"
|
||||
initialHost?.defaultPath || "/",
|
||||
);
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -184,7 +184,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
|
||||
const handleCloseWithError = useCallback(
|
||||
(errorMessage: string) => {
|
||||
if (isClosing) return; // Prevent duplicate calls
|
||||
if (isClosing) return;
|
||||
setIsClosing(true);
|
||||
toast.error(errorMessage);
|
||||
if (onClose) {
|
||||
@@ -758,10 +758,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const windowCount = Date.now() % 10;
|
||||
const baseOffsetX = 120 + windowCount * 30;
|
||||
const baseOffsetY = 120 + windowCount * 30;
|
||||
|
||||
|
||||
const maxOffsetX = Math.max(0, window.innerWidth - 800 - 100);
|
||||
const maxOffsetY = Math.max(0, window.innerHeight - 600 - 100);
|
||||
|
||||
|
||||
const offsetX = Math.min(baseOffsetX, maxOffsetX);
|
||||
const offsetY = Math.min(baseOffsetY, maxOffsetY);
|
||||
|
||||
|
||||
@@ -355,11 +355,22 @@ export function FileViewer({
|
||||
setShowLargeFileWarning(false);
|
||||
}
|
||||
|
||||
if (fileTypeInfo.type === "image" && file.name.toLowerCase().endsWith('.svg') && content) {
|
||||
if (
|
||||
fileTypeInfo.type === "image" &&
|
||||
file.name.toLowerCase().endsWith(".svg") &&
|
||||
content
|
||||
) {
|
||||
setImageLoading(false);
|
||||
setImageLoadError(false);
|
||||
}
|
||||
}, [content, savedContent, fileTypeInfo.type, isLargeFile, forceShowAsText, file.name]);
|
||||
}, [
|
||||
content,
|
||||
savedContent,
|
||||
fileTypeInfo.type,
|
||||
isLargeFile,
|
||||
forceShowAsText,
|
||||
file.name,
|
||||
]);
|
||||
|
||||
const handleContentChange = (newContent: string) => {
|
||||
setEditedContent(newContent);
|
||||
@@ -706,8 +717,8 @@ export function FileViewer({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : file.name.toLowerCase().endsWith('.svg') ? (
|
||||
<div
|
||||
) : file.name.toLowerCase().endsWith(".svg") ? (
|
||||
<div
|
||||
className="max-w-full max-h-full flex items-center justify-center"
|
||||
style={{ maxHeight: "calc(100vh - 200px)" }}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
|
||||
@@ -9,15 +9,15 @@ interface DragAndDropState {
|
||||
interface UseDragAndDropProps {
|
||||
onFilesDropped: (files: FileList) => void;
|
||||
onError?: (error: string) => void;
|
||||
maxFileSize?: number; // in MB
|
||||
maxFileSize?: number;
|
||||
allowedTypes?: string[];
|
||||
}
|
||||
|
||||
export function useDragAndDrop({
|
||||
onFilesDropped,
|
||||
onError,
|
||||
maxFileSize = 5120, // 5GB default - much more reasonable
|
||||
allowedTypes = [], // empty means all types allowed
|
||||
maxFileSize = 5120,
|
||||
allowedTypes = [],
|
||||
}: UseDragAndDropProps) {
|
||||
const [state, setState] = useState<DragAndDropState>({
|
||||
isDragging: false,
|
||||
@@ -32,28 +32,23 @@ export function useDragAndDrop({
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
// Check file size
|
||||
if (file.size > maxSizeBytes) {
|
||||
return `File "${file.name}" is too large. Maximum size is ${maxFileSize}MB.`;
|
||||
}
|
||||
|
||||
// Check file type if restrictions exist
|
||||
if (allowedTypes.length > 0) {
|
||||
const fileExt = file.name.split(".").pop()?.toLowerCase();
|
||||
const mimeType = file.type.toLowerCase();
|
||||
|
||||
const isAllowed = allowedTypes.some((type) => {
|
||||
// Check by extension
|
||||
if (type.startsWith(".")) {
|
||||
return fileExt === type.slice(1);
|
||||
}
|
||||
// Check by MIME type
|
||||
if (type.includes("/")) {
|
||||
return (
|
||||
mimeType === type || mimeType.startsWith(type.replace("*", ""))
|
||||
);
|
||||
}
|
||||
// Check by category
|
||||
switch (type) {
|
||||
case "image":
|
||||
return mimeType.startsWith("image/");
|
||||
@@ -114,7 +109,6 @@ export function useDragAndDrop({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Set dropEffect to indicate what operation is allowed
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -214,7 +214,11 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
);
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current || wasDisconnectedBySSH.current) {
|
||||
if (
|
||||
isUnmountingRef.current ||
|
||||
shouldNotReconnectRef.current ||
|
||||
wasDisconnectedBySSH.current
|
||||
) {
|
||||
isReconnectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ function AppContent() {
|
||||
isAdmin={isAdmin}
|
||||
username={username}
|
||||
>
|
||||
<div
|
||||
<div
|
||||
className="h-screen w-full visible pointer-events-auto static overflow-hidden"
|
||||
style={{ display: showTerminalView ? "block" : "none" }}
|
||||
>
|
||||
|
||||
@@ -82,7 +82,6 @@ export function HomepageAuth({
|
||||
const [registrationAllowed, setRegistrationAllowed] = useState(true);
|
||||
const [oidcConfigured, setOidcConfigured] = useState(false);
|
||||
|
||||
// Legacy reset states (kept for compatibility)
|
||||
const [resetStep, setResetStep] = useState<
|
||||
"initiate" | "verify" | "newPassword"
|
||||
>("initiate");
|
||||
@@ -106,8 +105,7 @@ export function HomepageAuth({
|
||||
const clearJWTOnLoad = async () => {
|
||||
try {
|
||||
await logoutUser();
|
||||
} catch (error) {
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
clearJWTOnLoad();
|
||||
@@ -242,7 +240,6 @@ export function HomepageAuth({
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
// HttpOnly cookies cannot be cleared from JavaScript - backend handles this
|
||||
if (err?.response?.data?.error?.includes("Database")) {
|
||||
setDbConnectionFailed(true);
|
||||
} else {
|
||||
@@ -253,8 +250,6 @@ export function HomepageAuth({
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Legacy password reset functions (deprecated) =====
|
||||
|
||||
async function handleInitiatePasswordReset() {
|
||||
setError(null);
|
||||
setResetLoading(true);
|
||||
@@ -317,7 +312,6 @@ export function HomepageAuth({
|
||||
setResetSuccess(true);
|
||||
toast.success(t("messages.passwordResetSuccess"));
|
||||
|
||||
// Immediately redirect to login after successful reset
|
||||
setTab("login");
|
||||
resetPasswordState();
|
||||
} catch (err: any) {
|
||||
@@ -372,7 +366,7 @@ export function HomepageAuth({
|
||||
setUsername(res.username || null);
|
||||
setUserId(res.userId || null);
|
||||
setDbError(null);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
onAuthSuccess({
|
||||
isAdmin: !!res.is_admin,
|
||||
@@ -380,7 +374,7 @@ export function HomepageAuth({
|
||||
userId: res.userId || null,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
@@ -392,7 +386,7 @@ export function HomepageAuth({
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
t("errors.invalidTotpCode");
|
||||
|
||||
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
|
||||
@@ -352,7 +352,7 @@ export function HomepageAuth({
|
||||
setUsername(res.username || null);
|
||||
setUserId(res.userId || null);
|
||||
setDbError(null);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
onAuthSuccess({
|
||||
isAdmin: !!res.is_admin,
|
||||
@@ -360,7 +360,7 @@ export function HomepageAuth({
|
||||
userId: res.userId || null,
|
||||
});
|
||||
}, 100);
|
||||
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
@@ -372,7 +372,7 @@ export function HomepageAuth({
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
t("errors.invalidTotpCode");
|
||||
|
||||
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
@@ -566,7 +566,9 @@ export function HomepageAuth({
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full h-11 text-base font-semibold"
|
||||
onClick={() => window.open("https://docs.termix.site/install", "_blank")}
|
||||
onClick={() =>
|
||||
window.open("https://docs.termix.site/install", "_blank")
|
||||
}
|
||||
>
|
||||
{t("mobile.viewMobileAppDocs")}
|
||||
</Button>
|
||||
@@ -900,7 +902,9 @@ export function HomepageAuth({
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full h-11 text-base font-semibold"
|
||||
onClick={() => window.open("https://docs.termix.site/install", "_blank")}
|
||||
onClick={() =>
|
||||
window.open("https://docs.termix.site/install", "_blank")
|
||||
}
|
||||
>
|
||||
{t("mobile.viewMobileAppDocs")}
|
||||
</Button>
|
||||
|
||||
@@ -164,7 +164,9 @@ const AppContent: FC = () => {
|
||||
</p>
|
||||
<button
|
||||
className="mt-4 px-6 py-3 bg-primary text-primary-foreground rounded-lg font-semibold hover:bg-primary/90 transition-colors"
|
||||
onClick={() => window.open("https://docs.termix.site/install", "_blank")}
|
||||
onClick={() =>
|
||||
window.open("https://docs.termix.site/install", "_blank")
|
||||
}
|
||||
>
|
||||
{t("mobile.viewMobileAppDocs")}
|
||||
</button>
|
||||
|
||||
@@ -58,14 +58,10 @@ interface LeftSidebarProps {
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
// Call backend logout endpoint to clear HttpOnly cookie and data session
|
||||
await logoutUser();
|
||||
|
||||
// Reload the page to reset the application state
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
// Even if logout fails, reload the page to reset state
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user