파일/디렉터리 생성/이름 변경시 로그를 남기도록 변경

This commit is contained in:
static
2025-01-12 19:02:21 +09:00
parent aebbc6d0c0
commit f8115f4f2e
8 changed files with 74 additions and 32 deletions

View File

@@ -1,13 +1,13 @@
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import db from "./drizzle"; import db from "./drizzle";
import { IntegrityError } from "./error"; import { IntegrityError } from "./error";
import { directory, file, mek } from "./schema"; import { directory, directoryLog, file, fileLog, mek } from "./schema";
type DirectoryId = "root" | number; type DirectoryId = "root" | number;
export interface NewDirectoryParams { export interface NewDirectoryParams {
userId: number;
parentId: DirectoryId; parentId: DirectoryId;
userId: number;
mekVersion: number; mekVersion: number;
encDek: string; encDek: string;
dekVersion: Date; dekVersion: Date;
@@ -16,9 +16,9 @@ export interface NewDirectoryParams {
} }
export interface NewFileParams { export interface NewFileParams {
path: string;
parentId: DirectoryId; parentId: DirectoryId;
userId: number; userId: number;
path: string;
mekVersion: number; mekVersion: number;
encDek: string; encDek: string;
dekVersion: Date; dekVersion: Date;
@@ -40,14 +40,23 @@ export const registerDirectory = async (params: NewDirectoryParams) => {
throw new IntegrityError("Inactive MEK version"); throw new IntegrityError("Inactive MEK version");
} }
await tx.insert(directory).values({ const newDirectories = await tx
createdAt: new Date(), .insert(directory)
parentId: params.parentId === "root" ? null : params.parentId, .values({
userId: params.userId, parentId: params.parentId === "root" ? null : params.parentId,
mekVersion: params.mekVersion, userId: params.userId,
encDek: params.encDek, mekVersion: params.mekVersion,
dekVersion: params.dekVersion, encDek: params.encDek,
encName: { ciphertext: params.encName, iv: params.encNameIv }, dekVersion: params.dekVersion,
encName: { ciphertext: params.encName, iv: params.encNameIv },
})
.returning({ id: directory.id });
const { id: directoryId } = newDirectories[0]!;
await tx.insert(directoryLog).values({
directoryId,
timestamp: new Date(),
action: "create",
newName: { ciphertext: params.encName, iv: params.encNameIv },
}); });
}, },
{ behavior: "exclusive" }, { behavior: "exclusive" },
@@ -99,6 +108,12 @@ export const setDirectoryEncName = async (
.update(directory) .update(directory)
.set({ encName: { ciphertext: encName, iv: encNameIv } }) .set({ encName: { ciphertext: encName, iv: encNameIv } })
.where(and(eq(directory.userId, userId), eq(directory.id, directoryId))); .where(and(eq(directory.userId, userId), eq(directory.id, directoryId)));
await tx.insert(directoryLog).values({
directoryId,
timestamp: new Date(),
action: "rename",
newName: { ciphertext: encName, iv: encNameIv },
});
}, },
{ behavior: "exclusive" }, { behavior: "exclusive" },
); );
@@ -148,17 +163,26 @@ export const registerFile = async (params: NewFileParams) => {
throw new IntegrityError("Inactive MEK version"); throw new IntegrityError("Inactive MEK version");
} }
await tx.insert(file).values({ const newFiles = await tx
path: params.path, .insert(file)
parentId: params.parentId === "root" ? null : params.parentId, .values({
createdAt: new Date(), path: params.path,
userId: params.userId, parentId: params.parentId === "root" ? null : params.parentId,
mekVersion: params.mekVersion, userId: params.userId,
contentType: params.contentType, mekVersion: params.mekVersion,
encDek: params.encDek, contentType: params.contentType,
dekVersion: params.dekVersion, encDek: params.encDek,
encContentIv: params.encContentIv, dekVersion: params.dekVersion,
encName: { ciphertext: params.encName, iv: params.encNameIv }, encContentIv: params.encContentIv,
encName: { ciphertext: params.encName, iv: params.encNameIv },
})
.returning({ id: file.id });
const { id: fileId } = newFiles[0]!;
await tx.insert(fileLog).values({
fileId,
timestamp: new Date(),
action: "create",
newName: { ciphertext: params.encName, iv: params.encNameIv },
}); });
}, },
{ behavior: "exclusive" }, { behavior: "exclusive" },
@@ -210,6 +234,12 @@ export const setFileEncName = async (
.update(file) .update(file)
.set({ encName: { ciphertext: encName, iv: encNameIv } }) .set({ encName: { ciphertext: encName, iv: encNameIv } })
.where(and(eq(file.userId, userId), eq(file.id, fileId))); .where(and(eq(file.userId, userId), eq(file.id, fileId)));
await tx.insert(fileLog).values({
fileId,
timestamp: new Date(),
action: "rename",
newName: { ciphertext: encName, iv: encNameIv },
});
}, },
{ behavior: "exclusive" }, { behavior: "exclusive" },
); );

View File

@@ -12,7 +12,6 @@ export const directory = sqliteTable(
"directory", "directory",
{ {
id: integer("id").primaryKey({ autoIncrement: true }), id: integer("id").primaryKey({ autoIncrement: true }),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
parentId: integer("parent_id"), parentId: integer("parent_id"),
userId: integer("user_id") userId: integer("user_id")
.notNull() .notNull()
@@ -34,16 +33,25 @@ export const directory = sqliteTable(
}), }),
); );
export const directoryLog = sqliteTable("directory_log", {
id: integer("id").primaryKey({ autoIncrement: true }),
directoryId: integer("directory_id")
.notNull()
.references(() => directory.id, { onDelete: "cascade" }),
timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(),
action: text("action", { enum: ["create", "rename"] }).notNull(),
newName: ciphertext("new_name"),
});
export const file = sqliteTable( export const file = sqliteTable(
"file", "file",
{ {
id: integer("id").primaryKey({ autoIncrement: true }), id: integer("id").primaryKey({ autoIncrement: true }),
path: text("path").notNull().unique(),
parentId: integer("parent_id").references(() => directory.id), parentId: integer("parent_id").references(() => directory.id),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
userId: integer("user_id") userId: integer("user_id")
.notNull() .notNull()
.references(() => user.id), .references(() => user.id),
path: text("path").notNull().unique(),
mekVersion: integer("master_encryption_key_version").notNull(), mekVersion: integer("master_encryption_key_version").notNull(),
encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64
dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(),
@@ -58,3 +66,13 @@ export const file = sqliteTable(
}), }),
}), }),
); );
export const fileLog = sqliteTable("file_log", {
id: integer("id").primaryKey({ autoIncrement: true }),
fileId: integer("file_id")
.notNull()
.references(() => file.id, { onDelete: "cascade" }),
timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(),
action: text("action", { enum: ["create", "rename"] }).notNull(),
newName: ciphertext("new_name"),
});

View File

@@ -3,7 +3,6 @@ import { z } from "zod";
export const directoryInfoResponse = z.object({ export const directoryInfoResponse = z.object({
metadata: z metadata: z
.object({ .object({
createdAt: z.string().datetime(),
mekVersion: z.number().int().positive(), mekVersion: z.number().int().positive(),
dek: z.string().base64().nonempty(), dek: z.string().base64().nonempty(),
dekVersion: z.string().datetime(), dekVersion: z.string().datetime(),

View File

@@ -2,7 +2,6 @@ import mime from "mime";
import { z } from "zod"; import { z } from "zod";
export const fileInfoResponse = z.object({ export const fileInfoResponse = z.object({
createdAt: z.string().datetime(),
mekVersion: z.number().int().positive(), mekVersion: z.number().int().positive(),
dek: z.string().base64().nonempty(), dek: z.string().base64().nonempty(),
dekVersion: z.string().datetime(), dekVersion: z.string().datetime(),

View File

@@ -22,7 +22,6 @@ export const getDirectoryInformation = async (userId: number, directoryId: "root
return { return {
metadata: directory && { metadata: directory && {
createdAt: directory.createdAt,
mekVersion: directory.mekVersion, mekVersion: directory.mekVersion,
encDek: directory.encDek, encDek: directory.encDek,
dekVersion: directory.dekVersion, dekVersion: directory.dekVersion,

View File

@@ -21,7 +21,6 @@ export const getFileInformation = async (userId: number, fileId: number) => {
} }
return { return {
createdAt: file.createdAt,
mekVersion: file.mekVersion, mekVersion: file.mekVersion,
encDek: file.encDek, encDek: file.encDek,
dekVersion: file.dekVersion, dekVersion: file.dekVersion,

View File

@@ -20,7 +20,6 @@ export const GET: RequestHandler = async ({ locals, params }) => {
return json( return json(
directoryInfoResponse.parse({ directoryInfoResponse.parse({
metadata: metadata && { metadata: metadata && {
createdAt: metadata.createdAt.toISOString(),
mekVersion: metadata.mekVersion, mekVersion: metadata.mekVersion,
dek: metadata.encDek, dek: metadata.encDek,
dekVersion: metadata.dekVersion.toISOString(), dekVersion: metadata.dekVersion.toISOString(),

View File

@@ -16,11 +16,10 @@ export const GET: RequestHandler = async ({ locals, params }) => {
if (!zodRes.success) error(400, "Invalid path parameters"); if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data; const { id } = zodRes.data;
const { createdAt, mekVersion, encDek, dekVersion, contentType, encContentIv, encName } = const { mekVersion, encDek, dekVersion, contentType, encContentIv, encName } =
await getFileInformation(userId, id); await getFileInformation(userId, id);
return json( return json(
fileInfoResponse.parse({ fileInfoResponse.parse({
createdAt: createdAt.toISOString(),
mekVersion, mekVersion,
dek: encDek, dek: encDek,
dekVersion: dekVersion.toISOString(), dekVersion: dekVersion.toISOString(),