72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
import { getActivePinia } from "pinia";
|
|
import { useReadOnlyUnlockStore, type WriteUnlockDurationSecs } from "@/stores/readOnlyUnlockStore";
|
|
import { classifyRedisCommandSafety } from "@/lib/redis/redisCommandSafety";
|
|
import { isRedisCommentLine } from "@/lib/redis/redisCommandTokenizer";
|
|
import { classifySqlRisk, isSqlRiskMutation } from "@/lib/sql/sqlRisk";
|
|
import type { DatabaseType } from "@/types/database";
|
|
|
|
export type { WriteUnlockDurationSecs };
|
|
|
|
export const WRITE_UNLOCK_DURATIONS: WriteUnlockDurationSecs[] = [60, 300];
|
|
|
|
export function formatWriteUnlockRemaining(ms: number): string {
|
|
const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|
const seconds = totalSeconds % 60;
|
|
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
}
|
|
|
|
function unlockStoreOrNull() {
|
|
try {
|
|
if (!getActivePinia()) return null;
|
|
return useReadOnlyUnlockStore();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function isWriteUnlockActive(connectionId: string | undefined | null): boolean {
|
|
if (!connectionId) return false;
|
|
return unlockStoreOrNull()?.isUnlocked(connectionId) === true;
|
|
}
|
|
|
|
export function connectionIsEffectivelyReadOnly(connection?: { id?: string; read_only?: boolean } | null): boolean {
|
|
if (!connection?.read_only) return false;
|
|
return !isWriteUnlockActive(connection.id);
|
|
}
|
|
|
|
export function sqlLooksLikeMutation(sql: string | undefined, databaseType?: DatabaseType): boolean {
|
|
if (!sql?.trim()) return true;
|
|
if (databaseType === "redis") {
|
|
return sql.split("\n").some((line) => {
|
|
const command = line.trim();
|
|
return command.length > 0 && !isRedisCommentLine(command) && classifyRedisCommandSafety(command) !== "allowed";
|
|
});
|
|
}
|
|
return isSqlRiskMutation(classifySqlRisk(sql, { dialect: databaseType }).risk);
|
|
}
|
|
|
|
export interface ReadOnlyWriteAccessOptions {
|
|
connection?: { id?: string; name?: string; read_only?: boolean; db_type?: DatabaseType } | null;
|
|
sql?: string;
|
|
source?: string;
|
|
treatAsMutation?: boolean;
|
|
}
|
|
|
|
export async function ensureReadOnlyWriteAccess(options: ReadOnlyWriteAccessOptions): Promise<boolean> {
|
|
const connection = options.connection;
|
|
if (!connection?.read_only) return true;
|
|
if (!connection.id) return false;
|
|
const store = unlockStoreOrNull();
|
|
if (store?.isUnlocked(connection.id)) return true;
|
|
const isMutation = options.treatAsMutation === true || sqlLooksLikeMutation(options.sql, connection.db_type);
|
|
if (!isMutation) return true;
|
|
if (!store) return false;
|
|
return store.requestUnlock({
|
|
connectionId: connection.id,
|
|
connectionName: connection.name,
|
|
sql: options.sql,
|
|
source: options.source,
|
|
});
|
|
}
|