서버 코드를 Router, Service, Repository 계층으로 분리하고 오류 처리 및 파일 삭제 로직 개선

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
staticandClaude Opus 5.5 committed 2026-10-12 00:00:21 +09:00
1 parent ed732be999
commit 6a6aa1765b
72 files changed
+1928 -1509

No files matched your search

+30 -89
View File
@@ -1,11 +1,8 @@
import { TRPCError } from "@trpc/server";
import argon2 from "argon2";
import { z } from "zod";
import { ClientRepo, SessionRepo, UserRepo, IntegrityError } from "#lib/server/db/index.js";
import env from "#lib/server/loadenv.js";
import { cookieOptions } from "#lib/server/modules/auth.js";
import { generateChallenge, verifySignature, issueSessionId } from "#lib/server/modules/crypto.js";
import { router, publicProcedure, roleProcedure } from "../init.server";
import { cookieOptions } from "#lib/server/auth.js";
import { AuthService } from "#lib/server/services/index.js";
import { router, publicProcedure, roleProcedure } from "#trpc/init.server.js";
import { Base64Schema, IdSchema } from "#trpc/schemas.js";
const authRouter = router({
login: publicProcedure
@@ -16,18 +13,17 @@ const authRouter = router({
}),
)
.mutation(async ({ ctx, input }) => {
const user = await UserRepo.getUserByEmail(input.email);
if (!user || !(await argon2.verify(user.password, input.password))) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid email or password" });
}
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
await SessionRepo.createSession(user.id, sessionId, ctx.locals.ip, ctx.locals.userAgent);
const { sessionIdSigned } = await AuthService.login(
input.email,
input.password,
ctx.locals.ip,
ctx.locals.userAgent,
);
ctx.cookies.set("sessionId", sessionIdSigned, cookieOptions);
}),
logout: roleProcedure["any"].mutation(async ({ ctx }) => {
await SessionRepo.deleteSession(ctx.session.sessionId);
await AuthService.logout(ctx.session.sessionId);
ctx.cookies.delete("sessionId", cookieOptions);
}),
@@ -39,103 +35,48 @@ const authRouter = router({
}),
)
.mutation(async ({ ctx, input }) => {
if (input.oldPassword === input.newPassword) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Same passwords" });
} else if (input.newPassword.length < 8) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Too short password" });
}
const user = await UserRepo.getUser(ctx.session.userId);
if (!user) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" });
} else if (!(await argon2.verify(user.password, input.oldPassword))) {
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid password" });
}
await UserRepo.setUserPassword(ctx.session.userId, await argon2.hash(input.newPassword));
await SessionRepo.deleteAllOtherSessions(ctx.session.userId, ctx.session.sessionId);
await AuthService.changePassword(
ctx.session.userId,
ctx.session.sessionId,
input.oldPassword,
input.newPassword,
);
}),
upgrade: roleProcedure["notClient"]
.input(
z.object({
encPubKey: z.base64().nonempty(),
sigPubKey: z.base64().nonempty(),
encPubKey: Base64Schema,
sigPubKey: Base64Schema,
}),
)
.mutation(async ({ ctx, input }) => {
const client = await ClientRepo.getClientByPubKeys(input.encPubKey, input.sigPubKey);
const userClient = client
? await ClientRepo.getUserClient(ctx.session.userId, client.id)
: undefined;
if (!client) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid public key(s)" });
} else if (!userClient || userClient.state === "challenging") {
throw new TRPCError({ code: "FORBIDDEN", message: "Unregistered client" });
}
const { answer, challenge } = await generateChallenge(32, input.encPubKey);
const { id } = await SessionRepo.registerSessionUpgradeChallenge(
return await AuthService.requestSessionUpgrade(
ctx.session.userId,
ctx.session.sessionId,
client.id,
answer.toString("base64"),
ctx.locals.ip,
new Date(Date.now() + env.challenge.sessionUpgradeExp),
input.encPubKey,
input.sigPubKey,
);
return { id, challenge: challenge.toString("base64") };
}),
verifyUpgrade: roleProcedure["notClient"]
.input(
z.object({
id: z.int().positive(),
answerSig: z.base64().nonempty(),
id: IdSchema,
answerSig: Base64Schema,
force: z.boolean().default(false),
}),
)
.mutation(async ({ ctx, input }) => {
const challenge = await SessionRepo.consumeSessionUpgradeChallenge(
input.id,
await AuthService.verifySessionUpgrade(
ctx.session.userId,
ctx.session.sessionId,
ctx.locals.ip,
input.id,
input.answerSig,
input.force,
);
if (!challenge) {
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer" });
}
const client = await ClientRepo.getClient(challenge.clientId);
if (!client) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid challenge answer" });
} else if (
!verifySignature(Buffer.from(challenge.answer, "base64"), input.answerSig, client.sigPubKey)
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Invalid challenge answer signature",
});
}
try {
await SessionRepo.upgradeSession(
ctx.session.userId,
ctx.session.sessionId,
client.id,
input.force,
);
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "Session not found") {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Invalid challenge answer",
});
} else if (!input.force && e.message === "Session already exists") {
throw new TRPCError({ code: "CONFLICT", message: "Already logged in" });
}
}
throw e;
}
}),
});
+45 -146
View File
@@ -1,8 +1,14 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { CategoryIdSchema } from "#lib/schemas/index.js";
import { CategoryRepo, FileRepo, IntegrityError } from "#lib/server/db/index.js";
import { router, roleProcedure } from "../init.server";
import { CategoryService } from "#lib/server/services/index.js";
import { toCategoryDto, toFileDto } from "#trpc/dto.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import {
Base64Schema,
CategoryIdSchema,
EncryptedNameSchema,
IdSchema,
toCiphertext,
} from "#trpc/schemas.js";
const categoryRouter = router({
get: roleProcedure["activeClient"]
@@ -13,53 +19,15 @@ const categoryRouter = router({
}),
)
.query(async ({ ctx, input }) => {
const category =
input.id !== "root"
? await CategoryRepo.getCategory(ctx.session.userId, input.id)
: undefined;
if (category === null) {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" });
}
const [categories, files] = await Promise.all([
CategoryRepo.getAllCategoriesByParent(ctx.session.userId, input.id),
input.id !== "root"
? FileRepo.getAllFilesByCategory(ctx.session.userId, input.id, input.recurse)
: undefined,
]);
const { category, subCategories, files } = await CategoryService.getCategory(
ctx.session.userId,
input.id,
input.recurse,
);
return {
metadata: category && {
parent: category.parentId,
mekVersion: category.mekVersion,
dek: category.encDek,
dekVersion: category.dekVersion,
name: category.encName.ciphertext,
nameIv: category.encName.iv,
},
subCategories: categories.map((category) => ({
id: category.id,
mekVersion: category.mekVersion,
dek: category.encDek,
dekVersion: category.dekVersion,
name: category.encName.ciphertext,
nameIv: category.encName.iv,
})),
files: files?.map((file) => ({
id: file.id,
parent: file.parentId,
mekVersion: file.mekVersion,
dek: file.encDek,
dekVersion: file.dekVersion,
contentType: file.contentType,
name: file.encName.ciphertext,
nameIv: file.encName.iv,
createdAt: file.encCreatedAt?.ciphertext,
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
isRecursive: file.isRecursive,
})),
metadata: category && toCategoryDto(category),
subCategories: subCategories.map(toCategoryDto),
files: files?.map((file) => ({ ...toFileDto(file), isRecursive: file.isRecursive })),
};
}),
@@ -67,139 +35,70 @@ const categoryRouter = router({
.input(
z.object({
parent: CategoryIdSchema,
mekVersion: z.int().positive(),
dek: z.base64().nonempty(),
mekVersion: IdSchema,
dek: Base64Schema,
dekVersion: z.date(),
name: z.base64().nonempty(),
nameIv: z.base64().nonempty(),
...EncryptedNameSchema,
}),
)
.mutation(async ({ ctx, input }) => {
const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
const oneMinuteLater = new Date(Date.now() + 60 * 1000);
if (input.dekVersion <= oneMinuteAgo || input.dekVersion >= oneMinuteLater) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid DEK version" });
}
try {
await CategoryRepo.registerCategory({
parentId: input.parent,
userId: ctx.session.userId,
mekVersion: input.mekVersion,
encDek: input.dek,
dekVersion: input.dekVersion,
encName: { ciphertext: input.name, iv: input.nameIv },
});
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "Inactive MEK version") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
} else if (e.message === "Category not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid parent category id" });
}
}
throw e;
}
await CategoryService.createCategory({
userId: ctx.session.userId,
parentId: input.parent,
mekVersion: input.mekVersion,
encDek: input.dek,
dekVersion: input.dekVersion,
encName: toCiphertext(input.name, input.nameIv),
});
}),
rename: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
id: IdSchema,
dekVersion: z.date(),
name: z.base64().nonempty(),
nameIv: z.base64().nonempty(),
...EncryptedNameSchema,
}),
)
.mutation(async ({ ctx, input }) => {
try {
await CategoryRepo.setCategoryEncName(ctx.session.userId, input.id, input.dekVersion, {
ciphertext: input.name,
iv: input.nameIv,
});
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "Category not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" });
} else if (e.message === "Invalid DEK version") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
}
throw e;
}
await CategoryService.renameCategory(
ctx.session.userId,
input.id,
input.dekVersion,
toCiphertext(input.name, input.nameIv),
);
}),
delete: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
id: IdSchema,
}),
)
.mutation(async ({ ctx, input }) => {
try {
await CategoryRepo.unregisterCategory(ctx.session.userId, input.id);
} catch (e) {
if (e instanceof IntegrityError && e.message === "Category not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" });
}
throw e;
}
await CategoryService.deleteCategory(ctx.session.userId, input.id);
}),
addFile: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
file: z.int().positive(),
id: IdSchema,
file: IdSchema,
}),
)
.mutation(async ({ ctx, input }) => {
const [category, file] = await Promise.all([
CategoryRepo.getCategory(ctx.session.userId, input.id),
FileRepo.getFile(ctx.session.userId, input.file),
]);
if (!category) {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" });
} else if (!file) {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
}
try {
await FileRepo.addFileToCategory(input.file, input.id);
} catch (e) {
if (e instanceof IntegrityError && e.message === "File already added to category") {
throw new TRPCError({ code: "BAD_REQUEST", message: "File already added" });
}
throw e;
}
await CategoryService.addFileToCategory(ctx.session.userId, input.id, input.file);
}),
removeFile: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
file: z.int().positive(),
id: IdSchema,
file: IdSchema,
}),
)
.mutation(async ({ ctx, input }) => {
const [category, file] = await Promise.all([
CategoryRepo.getCategory(ctx.session.userId, input.id),
FileRepo.getFile(ctx.session.userId, input.file),
]);
if (!category) {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" });
} else if (!file) {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
}
try {
await FileRepo.removeFileFromCategory(input.file, input.id);
} catch (e) {
if (e instanceof IntegrityError && e.message === "File not found in category") {
throw new TRPCError({ code: "BAD_REQUEST", message: "File not added" });
}
throw e;
}
await CategoryService.removeFileFromCategory(ctx.session.userId, input.id, input.file);
}),
});
+16 -73
View File
@@ -1,96 +1,39 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { ClientRepo, IntegrityError } from "#lib/server/db/index.js";
import env from "#lib/server/loadenv.js";
import { verifyPubKey, verifySignature, generateChallenge } from "#lib/server/modules/crypto.js";
import { router, roleProcedure } from "../init.server";
const createUserClientChallenge = async (
ip: string,
userId: number,
clientId: number,
encPubKey: string,
) => {
const { answer, challenge } = await generateChallenge(32, encPubKey);
const { id } = await ClientRepo.registerUserClientChallenge(
userId,
clientId,
answer.toString("base64"),
ip,
new Date(Date.now() + env.challenge.userClientExp),
);
return { id, challenge: challenge.toString("base64") };
};
import { ClientService } from "#lib/server/services/index.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import { Base64Schema, IdSchema } from "#trpc/schemas.js";
const clientRouter = router({
register: roleProcedure["notClient"]
.input(
z.object({
encPubKey: z.base64().nonempty(),
sigPubKey: z.base64().nonempty(),
encPubKey: Base64Schema,
sigPubKey: Base64Schema,
}),
)
.mutation(async ({ ctx, input }) => {
const { userId } = ctx.session;
const { encPubKey, sigPubKey } = input;
const client = await ClientRepo.getClientByPubKeys(encPubKey, sigPubKey);
if (client) {
try {
await ClientRepo.createUserClient(userId, client.id);
return await createUserClientChallenge(ctx.locals.ip, userId, client.id, encPubKey);
} catch (e) {
if (e instanceof IntegrityError && e.message === "User client already exists") {
throw new TRPCError({ code: "CONFLICT", message: "Client already registered" });
}
throw e;
}
} else {
if (encPubKey === sigPubKey) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Same public keys" });
} else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid public key(s)" });
}
try {
const { id: clientId } = await ClientRepo.createClient(encPubKey, sigPubKey, userId);
return await createUserClientChallenge(ctx.locals.ip, userId, clientId, encPubKey);
} catch (e) {
if (e instanceof IntegrityError && e.message === "Public key(s) already registered") {
throw new TRPCError({ code: "CONFLICT", message: "Public key(s) already used" });
}
throw e;
}
}
return await ClientService.registerClient(
ctx.session.userId,
ctx.locals.ip,
input.encPubKey,
input.sigPubKey,
);
}),
verify: roleProcedure["notClient"]
.input(
z.object({
id: z.int().positive(),
answerSig: z.base64().nonempty(),
id: IdSchema,
answerSig: Base64Schema,
}),
)
.mutation(async ({ ctx, input }) => {
const challenge = await ClientRepo.consumeUserClientChallenge(
input.id,
await ClientService.verifyClient(
ctx.session.userId,
ctx.locals.ip,
input.id,
input.answerSig,
);
if (!challenge) {
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer" });
}
const client = await ClientRepo.getClient(challenge.clientId);
if (!client) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid challenge answer" });
} else if (
!verifySignature(Buffer.from(challenge.answer, "base64"), input.answerSig, client.sigPubKey)
) {
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer signature" });
}
await ClientRepo.setUserClientStateToPending(ctx.session.userId, client.id);
}),
});
+42 -112
View File
@@ -1,9 +1,14 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { DirectoryIdSchema } from "#lib/schemas/index.js";
import { DirectoryRepo, FileRepo, IntegrityError } from "#lib/server/db/index.js";
import { safeUnlink } from "#lib/server/modules/filesystem.js";
import { router, roleProcedure } from "../init.server";
import { DirectoryService } from "#lib/server/services/index.js";
import { toDirectoryDto, toFileDto } from "#trpc/dto.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import {
Base64Schema,
DirectoryIdSchema,
EncryptedNameSchema,
IdSchema,
toCiphertext,
} from "#trpc/schemas.js";
const directoryRouter = router({
get: roleProcedure["activeClient"]
@@ -13,51 +18,14 @@ const directoryRouter = router({
}),
)
.query(async ({ ctx, input }) => {
const directory =
input.id !== "root"
? await DirectoryRepo.getDirectory(ctx.session.userId, input.id)
: undefined;
if (directory === null) {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" });
}
const [directories, files] = await Promise.all([
DirectoryRepo.getAllDirectoriesByParent(ctx.session.userId, input.id),
FileRepo.getAllFilesByParent(ctx.session.userId, input.id),
]);
const { directory, subDirectories, files } = await DirectoryService.getDirectory(
ctx.session.userId,
input.id,
);
return {
metadata: directory && {
parent: directory.parentId,
mekVersion: directory.mekVersion,
dek: directory.encDek,
dekVersion: directory.dekVersion,
name: directory.encName.ciphertext,
nameIv: directory.encName.iv,
isFavorite: directory.isFavorite,
},
subDirectories: directories.map((directory) => ({
id: directory.id,
mekVersion: directory.mekVersion,
dek: directory.encDek,
dekVersion: directory.dekVersion,
name: directory.encName.ciphertext,
nameIv: directory.encName.iv,
isFavorite: directory.isFavorite,
})),
files: files.map((file) => ({
id: file.id,
mekVersion: file.mekVersion,
dek: file.encDek,
dekVersion: file.dekVersion,
contentType: file.contentType,
name: file.encName.ciphertext,
nameIv: file.encName.iv,
createdAt: file.encCreatedAt?.ciphertext,
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
})),
metadata: directory && toDirectoryDto(directory),
subDirectories: subDirectories.map(toDirectoryDto),
files: files.map(toFileDto),
};
}),
@@ -65,90 +33,52 @@ const directoryRouter = router({
.input(
z.object({
parent: DirectoryIdSchema,
mekVersion: z.int().positive(),
dek: z.base64().nonempty(),
mekVersion: IdSchema,
dek: Base64Schema,
dekVersion: z.date(),
name: z.base64().nonempty(),
nameIv: z.base64().nonempty(),
...EncryptedNameSchema,
}),
)
.mutation(async ({ ctx, input }) => {
const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
const oneMinuteLater = new Date(Date.now() + 60 * 1000);
if (input.dekVersion <= oneMinuteAgo || input.dekVersion >= oneMinuteLater) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid DEK version" });
}
try {
await DirectoryRepo.registerDirectory({
parentId: input.parent,
userId: ctx.session.userId,
mekVersion: input.mekVersion,
encDek: input.dek,
dekVersion: input.dekVersion,
encName: { ciphertext: input.name, iv: input.nameIv },
});
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "Inactive MEK version") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
} else if (e.message === "Directory not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid parent directory id" });
}
}
throw e;
}
await DirectoryService.createDirectory({
userId: ctx.session.userId,
parentId: input.parent,
mekVersion: input.mekVersion,
encDek: input.dek,
dekVersion: input.dekVersion,
encName: toCiphertext(input.name, input.nameIv),
});
}),
rename: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
id: IdSchema,
dekVersion: z.date(),
name: z.base64().nonempty(),
nameIv: z.base64().nonempty(),
...EncryptedNameSchema,
}),
)
.mutation(async ({ ctx, input }) => {
try {
await DirectoryRepo.setDirectoryEncName(ctx.session.userId, input.id, input.dekVersion, {
ciphertext: input.name,
iv: input.nameIv,
});
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "Directory not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" });
} else if (e.message === "Invalid DEK version") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
}
throw e;
}
await DirectoryService.renameDirectory(
ctx.session.userId,
input.id,
input.dekVersion,
toCiphertext(input.name, input.nameIv),
);
}),
delete: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
id: IdSchema,
}),
)
.mutation(async ({ ctx, input }) => {
try {
const files = await DirectoryRepo.unregisterDirectory(ctx.session.userId, input.id);
return {
deletedFiles: files.map((file) => {
safeUnlink(file.path); // Intended
safeUnlink(file.thumbnailPath); // Intended
return file.id;
}),
};
} catch (e) {
if (e instanceof IntegrityError && e.message === "Directory not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" });
}
throw e;
}
const { deletedFileIds } = await DirectoryService.deleteDirectory(
ctx.session.userId,
input.id,
);
return { deletedFiles: deletedFileIds };
}),
});
+19 -99
View File
@@ -1,123 +1,43 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { DirectoryRepo, FileRepo, IntegrityError } from "#lib/server/db/index.js";
import { router, roleProcedure } from "../init.server";
import { DirectoryService, FileService } from "#lib/server/services/index.js";
import { toDirectoryDto, toFileDto } from "#trpc/dto.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import { IdSchema } from "#trpc/schemas.js";
const IdInputSchema = z.object({ id: IdSchema });
const favoritesRouter = router({
get: roleProcedure["activeClient"].query(async ({ ctx }) => {
const [directories, files] = await Promise.all([
DirectoryRepo.getAllFavoriteDirectories(ctx.session.userId),
FileRepo.getAllFavoriteFiles(ctx.session.userId),
DirectoryService.getFavoriteDirectories(ctx.session.userId),
FileService.getFavoriteFiles(ctx.session.userId),
]);
return {
directories: directories.map((directory) => ({
id: directory.id,
parent: directory.parentId,
mekVersion: directory.mekVersion,
dek: directory.encDek,
dekVersion: directory.dekVersion,
name: directory.encName.ciphertext,
nameIv: directory.encName.iv,
})),
files: files.map((file) => ({
id: file.id,
parent: file.parentId,
mekVersion: file.mekVersion,
dek: file.encDek,
dekVersion: file.dekVersion,
contentType: file.contentType,
name: file.encName.ciphertext,
nameIv: file.encName.iv,
createdAt: file.encCreatedAt?.ciphertext,
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
})),
directories: directories.map(toDirectoryDto),
files: files.map(toFileDto),
};
}),
addDirectory: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
}),
)
.input(IdInputSchema)
.mutation(async ({ ctx, input }) => {
try {
await DirectoryRepo.setDirectoryFavorite(ctx.session.userId, input.id, true);
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "Directory not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" });
} else if (e.message === "Directory already favorited") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
}
throw e;
}
await DirectoryService.setDirectoryFavorite(ctx.session.userId, input.id, true);
}),
removeDirectory: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
}),
)
.input(IdInputSchema)
.mutation(async ({ ctx, input }) => {
try {
await DirectoryRepo.setDirectoryFavorite(ctx.session.userId, input.id, false);
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "Directory not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" });
} else if (e.message === "Directory not favorited") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
}
throw e;
}
await DirectoryService.setDirectoryFavorite(ctx.session.userId, input.id, false);
}),
addFile: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
}),
)
.mutation(async ({ ctx, input }) => {
try {
await FileRepo.setFileFavorite(ctx.session.userId, input.id, true);
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "File not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
} else if (e.message === "File already favorited") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
}
throw e;
}
}),
addFile: roleProcedure["activeClient"].input(IdInputSchema).mutation(async ({ ctx, input }) => {
await FileService.setFileFavorite(ctx.session.userId, input.id, true);
}),
removeFile: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
}),
)
.input(IdInputSchema)
.mutation(async ({ ctx, input }) => {
try {
await FileRepo.setFileFavorite(ctx.session.userId, input.id, false);
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "File not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
} else if (e.message === "File not favorited") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
}
throw e;
}
await FileService.setFileFavorite(ctx.session.userId, input.id, false);
}),
});
+28 -119
View File
@@ -1,97 +1,44 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { FileRepo, MediaRepo, IntegrityError } from "#lib/server/db/index.js";
import { safeUnlink } from "#lib/server/modules/filesystem.js";
import { router, roleProcedure } from "../init.server";
import { FileService } from "#lib/server/services/index.js";
import { toFileDto, toFileWithCategoriesDto } from "#trpc/dto.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import { Base64Schema, EncryptedNameSchema, IdSchema, toCiphertext } from "#trpc/schemas.js";
const fileRouter = router({
get: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
id: IdSchema,
}),
)
.query(async ({ ctx, input }) => {
const file = await FileRepo.getFile(ctx.session.userId, input.id);
if (!file) {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
}
const categories = await FileRepo.getAllFileCategories(input.id);
return {
isLegacy: !!file.encContentIv,
parent: file.parentId,
mekVersion: file.mekVersion,
dek: file.encDek,
dekVersion: file.dekVersion,
contentType: file.contentType,
name: file.encName.ciphertext,
nameIv: file.encName.iv,
createdAt: file.encCreatedAt?.ciphertext,
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
categories: categories.map((category) => ({
id: category.id,
parent: category.parentId,
mekVersion: category.mekVersion,
dek: category.encDek,
dekVersion: category.dekVersion,
name: category.encName.ciphertext,
nameIv: category.encName.iv,
})),
};
return toFileWithCategoriesDto(await FileService.getFile(ctx.session.userId, input.id));
}),
bulkGet: roleProcedure["activeClient"]
.input(
z.object({
ids: z.number().positive().array(),
ids: IdSchema.array(),
}),
)
.query(async ({ ctx, input }) => {
const files = await FileRepo.getFilesWithCategories(ctx.session.userId, input.ids);
return files.map((file) => ({
id: file.id,
isLegacy: !!file.encContentIv,
parent: file.parentId,
mekVersion: file.mekVersion,
dek: file.encDek,
dekVersion: file.dekVersion,
contentType: file.contentType,
name: file.encName.ciphertext,
nameIv: file.encName.iv,
createdAt: file.encCreatedAt?.ciphertext,
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
categories: file.categories.map((category) => ({
id: category.id,
parent: category.parentId,
mekVersion: category.mekVersion,
dek: category.encDek,
dekVersion: category.dekVersion,
name: category.encName.ciphertext,
nameIv: category.encName.iv,
})),
}));
const files = await FileService.getFiles(ctx.session.userId, input.ids);
return files.map(toFileWithCategoriesDto);
}),
list: roleProcedure["activeClient"].query(async ({ ctx }) => {
return await FileRepo.getAllFileIds(ctx.session.userId);
return await FileService.getAllFileIds(ctx.session.userId);
}),
listByHash: roleProcedure["activeClient"]
.input(
z.object({
hskVersion: z.int().positive(),
contentHmac: z.base64().nonempty(),
hskVersion: IdSchema,
contentHmac: Base64Schema,
}),
)
.query(async ({ ctx, input }) => {
return await FileRepo.getAllFileIdsByContentHmac(
return await FileService.getFileIdsByContentHmac(
ctx.session.userId,
input.hskVersion,
input.contentHmac,
@@ -99,84 +46,46 @@ const fileRouter = router({
}),
listWithoutThumbnail: roleProcedure["activeClient"].query(async ({ ctx }) => {
const files = await FileRepo.getFilesWithoutThumbnail(ctx.session.userId);
return files.map((file) => ({
id: file.id,
isLegacy: !!file.encContentIv,
parent: file.parentId,
mekVersion: file.mekVersion,
dek: file.encDek,
dekVersion: file.dekVersion,
contentType: file.contentType,
name: file.encName.ciphertext,
nameIv: file.encName.iv,
createdAt: file.encCreatedAt?.ciphertext,
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
}));
const files = await FileService.getFilesWithoutThumbnail(ctx.session.userId);
return files.map(toFileDto);
}),
rename: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
id: IdSchema,
dekVersion: z.date(),
name: z.base64().nonempty(),
nameIv: z.base64().nonempty(),
...EncryptedNameSchema,
}),
)
.mutation(async ({ ctx, input }) => {
try {
await FileRepo.setFileEncName(ctx.session.userId, input.id, input.dekVersion, {
ciphertext: input.name,
iv: input.nameIv,
});
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "File not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
} else if (e.message === "Invalid DEK version") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
}
throw e;
}
await FileService.renameFile(
ctx.session.userId,
input.id,
input.dekVersion,
toCiphertext(input.name, input.nameIv),
);
}),
delete: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
id: IdSchema,
}),
)
.mutation(async ({ ctx, input }) => {
try {
const { path, thumbnailPath } = await FileRepo.unregisterFile(ctx.session.userId, input.id);
safeUnlink(path); // Intended
safeUnlink(thumbnailPath); // Intended
} catch (e) {
if (e instanceof IntegrityError && e.message === "File not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
}
throw e;
}
await FileService.deleteFile(ctx.session.userId, input.id);
}),
thumbnail: roleProcedure["activeClient"]
.input(
z.object({
id: z.int().positive(),
id: IdSchema,
}),
)
.query(async ({ ctx, input }) => {
const thumbnail = await MediaRepo.getFileThumbnail(ctx.session.userId, input.id);
if (!thumbnail) {
throw new TRPCError({ code: "NOT_FOUND", message: "File or its thumbnail not found" });
}
return { updatedAt: thumbnail.updatedAt };
const { updatedAt } = await FileService.getFileThumbnail(ctx.session.userId, input.id);
return { updatedAt };
}),
});
+12 -19
View File
@@ -1,11 +1,11 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { HskRepo, IntegrityError } from "#lib/server/db/index.js";
import { router, roleProcedure } from "../init.server";
import { KeyService } from "#lib/server/services/index.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import { Base64Schema, IdSchema } from "#trpc/schemas.js";
const hskRouter = router({
list: roleProcedure["activeClient"].query(async ({ ctx }) => {
const hsks = await HskRepo.getAllValidHsks(ctx.session.userId);
const hsks = await KeyService.getHsks(ctx.session.userId);
return hsks.map(({ version, state, mekVersion, encHsk }) => ({
version,
state,
@@ -17,24 +17,17 @@ const hskRouter = router({
registerInitial: roleProcedure["activeClient"]
.input(
z.object({
mekVersion: z.int().positive(),
hsk: z.base64().nonempty(),
mekVersion: IdSchema,
hsk: Base64Schema,
}),
)
.mutation(async ({ ctx, input }) => {
try {
await HskRepo.registerInitialHsk(
ctx.session.userId,
ctx.session.clientId,
input.mekVersion,
input.hsk,
);
} catch (e) {
if (e instanceof IntegrityError && e.message === "HSK already registered") {
throw new TRPCError({ code: "CONFLICT", message: "Initial HSK already registered" });
}
throw e;
}
await KeyService.registerInitialHsk(
ctx.session.userId,
ctx.session.clientId,
input.mekVersion,
input.hsk,
);
}),
});
+12 -41
View File
@@ -1,31 +1,11 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { ClientRepo, MekRepo, IntegrityError } from "#lib/server/db/index.js";
import { verifySignature } from "#lib/server/modules/crypto.js";
import { router, roleProcedure } from "../init.server";
const verifyClientEncMekSig = async (
userId: number,
clientId: number,
version: number,
encMek: string,
encMekSig: string,
) => {
const userClient = await ClientRepo.getUserClientWithDetails(userId, clientId);
if (!userClient) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" });
}
const data = JSON.stringify({ version, key: encMek });
return verifySignature(Buffer.from(data), encMekSig, userClient.sigPubKey);
};
import { KeyService } from "#lib/server/services/index.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import { Base64Schema } from "#trpc/schemas.js";
const mekRouter = router({
list: roleProcedure["activeClient"].query(async ({ ctx }) => {
const clientMeks = await MekRepo.getAllValidClientMeks(
ctx.session.userId,
ctx.session.clientId,
);
const clientMeks = await KeyService.getClientMeks(ctx.session.userId, ctx.session.clientId);
return clientMeks.map(({ version, state, encMek, encMekSig }) => ({
version,
state,
@@ -37,26 +17,17 @@ const mekRouter = router({
registerInitial: roleProcedure["pendingClient"]
.input(
z.object({
mek: z.base64().nonempty(),
mekSig: z.base64().nonempty(),
mek: Base64Schema,
mekSig: Base64Schema,
}),
)
.mutation(async ({ ctx, input }) => {
const { userId, clientId } = ctx.session;
const { mek, mekSig } = input;
if (!(await verifyClientEncMekSig(userId, clientId, 1, mek, mekSig))) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid signature" });
}
try {
await MekRepo.registerInitialMek(userId, clientId, mek, mekSig);
await ClientRepo.setUserClientStateToActive(userId, clientId);
} catch (e) {
if (e instanceof IntegrityError && e.message === "MEK already registered") {
throw new TRPCError({ code: "CONFLICT", message: "Initial MEK already registered" });
}
throw e;
}
await KeyService.registerInitialMek(
ctx.session.userId,
ctx.session.clientId,
input.mek,
input.mekSig,
);
}),
});
+14 -44
View File
@@ -1,7 +1,8 @@
import { z } from "zod";
import { DirectoryIdSchema } from "#lib/schemas/index.js";
import { DirectoryRepo, FileRepo } from "#lib/server/db/index.js";
import { router, roleProcedure } from "../init.server";
import { SearchService } from "#lib/server/services/index.js";
import { toDirectoryDto, toFileDto } from "#trpc/dto.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import { DirectoryIdSchema, IdSchema } from "#trpc/schemas.js";
const searchRouter = router({
search: roleProcedure["activeClient"]
@@ -9,51 +10,20 @@ const searchRouter = router({
z.object({
ancestor: DirectoryIdSchema.default("root"),
inFavorites: z.boolean().default(false),
includeCategories: z.number().positive().array().default([]),
excludeCategories: z.number().positive().array().default([]),
includeCategories: IdSchema.array().default([]),
excludeCategories: IdSchema.array().default([]),
}),
)
.query(async ({ ctx, input }) => {
const [directories, files] = await Promise.all([
input.includeCategories.length === 0 && input.excludeCategories.length === 0
? DirectoryRepo.searchDirectories(ctx.session.userId, {
parentId: input.ancestor,
inFavorites: input.inFavorites,
})
: [],
FileRepo.searchFiles(ctx.session.userId, {
parentId: input.ancestor,
inFavorites: input.inFavorites,
includeCategoryIds: input.includeCategories,
excludeCategoryIds: input.excludeCategories,
}),
]);
const { directories, files } = await SearchService.search(ctx.session.userId, {
ancestorId: input.ancestor,
inFavorites: input.inFavorites,
includeCategoryIds: input.includeCategories,
excludeCategoryIds: input.excludeCategories,
});
return {
directories: directories.map((directory) => ({
id: directory.id,
parent: directory.parentId,
mekVersion: directory.mekVersion,
dek: directory.encDek,
dekVersion: directory.dekVersion,
name: directory.encName.ciphertext,
nameIv: directory.encName.iv,
isFavorite: directory.isFavorite,
})),
files: files.map((file) => ({
id: file.id,
parent: file.parentId,
mekVersion: file.mekVersion,
dek: file.encDek,
dekVersion: file.dekVersion,
contentType: file.contentType,
name: file.encName.ciphertext,
nameIv: file.encName.iv,
createdAt: file.encCreatedAt?.ciphertext,
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
})),
directories: directories.map(toDirectoryDto),
files: files.map(toFileDto),
};
}),
});
+50 -205
View File
@@ -1,206 +1,87 @@
import { TRPCError } from "@trpc/server";
import { createHash } from "crypto";
import { createReadStream } from "fs";
import { mkdir, open } from "fs/promises";
import mime from "mime";
import { dirname } from "path";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { DirectoryIdSchema } from "#lib/schemas/index.js";
import { FileRepo, MediaRepo, UploadRepo, IntegrityError } from "#lib/server/db/index.js";
import db from "#lib/server/db/kysely.js";
import env from "#lib/server/loadenv.js";
import { safeUnlink } from "#lib/server/modules/filesystem.js";
import { router, roleProcedure } from "../init.server";
const UPLOADS_EXPIRES = 24 * 3600 * 1000; // 24 hours
const sessionLocks = new Set<string>();
const reserveUploadPath = async (path: string) => {
await mkdir(dirname(path), { recursive: true });
const file = await open(path, "wx", 0o600);
await file.close();
};
const generateFileUploadSession = async (userId: number) => {
const id = uuidv4();
const path = `${env.libraryPath}/${userId}/${uuidv4()}`;
await reserveUploadPath(path);
return { id, path };
};
const generateThumbnailUploadSession = async (userId: number) => {
const id = uuidv4();
const path = `${env.thumbnailsPath}/${userId}/${id}`;
await reserveUploadPath(path);
return { id, path };
};
import { UploadService } from "#lib/server/services/index.js";
import { router, roleProcedure } from "#trpc/init.server.js";
import {
Base64Schema,
DirectoryIdSchema,
EncryptedNameSchema,
IdSchema,
toCiphertext,
} from "#trpc/schemas.js";
const uploadRouter = router({
startFileUpload: roleProcedure["activeClient"]
.input(
z.object({
chunks: z.int().positive(),
chunks: IdSchema,
parent: DirectoryIdSchema,
mekVersion: z.int().positive(),
dek: z.base64().nonempty(),
mekVersion: IdSchema,
dek: Base64Schema,
dekVersion: z.date(),
hskVersion: z.int().positive().optional(),
hskVersion: IdSchema.optional(),
contentType: z
.string()
.trim()
.nonempty()
.refine((value) => mime.getExtension(value) !== null),
name: z.base64().nonempty(),
nameIv: z.base64().nonempty(),
createdAt: z.base64().nonempty().optional(),
createdAtIv: z.base64().nonempty().optional(),
lastModifiedAt: z.base64().nonempty(),
lastModifiedAtIv: z.base64().nonempty(),
...EncryptedNameSchema,
createdAt: Base64Schema.optional(),
createdAtIv: Base64Schema.optional(),
lastModifiedAt: Base64Schema,
lastModifiedAtIv: Base64Schema,
}),
)
.mutation(async ({ ctx, input }) => {
const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
const oneMinuteLater = new Date(Date.now() + 60 * 1000);
if (input.dekVersion <= oneMinuteAgo || input.dekVersion >= oneMinuteLater) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid DEK version" });
}
const { id, path } = await generateFileUploadSession(ctx.session.userId);
try {
await UploadRepo.createFileUploadSession({
id,
userId: ctx.session.userId,
path,
totalChunks: input.chunks,
expiresAt: new Date(Date.now() + UPLOADS_EXPIRES),
parentId: input.parent,
mekVersion: input.mekVersion,
encDek: input.dek,
dekVersion: input.dekVersion,
hskVersion: input.hskVersion ?? null,
contentType: input.contentType,
encName: { ciphertext: input.name, iv: input.nameIv },
encCreatedAt:
input.createdAt && input.createdAtIv
? { ciphertext: input.createdAt, iv: input.createdAtIv }
: null,
encLastModifiedAt: { ciphertext: input.lastModifiedAt, iv: input.lastModifiedAtIv },
});
return { uploadId: id };
} catch (e) {
await safeUnlink(path);
if (e instanceof IntegrityError) {
if (e.message === "Inactive MEK version") {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid MEK version" });
} else if (e.message === "Inactive HSK version") {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid HSK version" });
} else if (e.message === "Directory not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid parent directory id" });
}
}
throw e;
}
return await UploadService.startFileUpload({
userId: ctx.session.userId,
totalChunks: input.chunks,
parentId: input.parent,
mekVersion: input.mekVersion,
encDek: input.dek,
dekVersion: input.dekVersion,
hskVersion: input.hskVersion ?? null,
contentType: input.contentType,
encName: toCiphertext(input.name, input.nameIv),
encCreatedAt:
input.createdAt && input.createdAtIv
? toCiphertext(input.createdAt, input.createdAtIv)
: null,
encLastModifiedAt: toCiphertext(input.lastModifiedAt, input.lastModifiedAtIv),
});
}),
completeFileUpload: roleProcedure["activeClient"]
.input(
z.object({
uploadId: z.uuidv4(),
contentHmac: z.base64().nonempty().optional(),
encContentHash: z.base64().nonempty(),
contentHmac: Base64Schema.optional(),
encContentHash: Base64Schema,
}),
)
.mutation(async ({ ctx, input }) => {
const { uploadId } = input;
if (sessionLocks.has(uploadId)) {
throw new TRPCError({ code: "CONFLICT", message: "Completion already in progress" });
} else {
sessionLocks.add(uploadId);
}
try {
const session = await UploadRepo.getUploadSession(uploadId, ctx.session.userId);
if (session?.type !== "file") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid upload id" });
} else if (
(session.hskVersion && !input.contentHmac) ||
(!session.hskVersion && input.contentHmac)
) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid content HMAC" });
} else if (session.uploadedChunks < session.totalChunks) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Upload not completed" });
}
const hashStream = createHash("sha256");
for await (const chunk of createReadStream(session.path)) {
hashStream.update(chunk);
}
const hash = hashStream.digest("base64");
if (hash !== input.encContentHash) {
await UploadRepo.deleteUploadSession(db, uploadId);
await safeUnlink(session.path);
throw new TRPCError({ code: "CONFLICT", message: "Uploaded file corrupted" });
}
const fileId = await db.transaction().execute(async (trx) => {
const { id: fileId } = await FileRepo.registerFile(trx, {
...session,
userId: ctx.session.userId,
path: session.path,
contentHmac: input.contentHmac ?? null,
encContentHash: hash,
encContentIv: null,
});
await UploadRepo.deleteUploadSession(trx, uploadId);
return fileId;
});
return { file: fileId };
} finally {
sessionLocks.delete(uploadId);
}
const { fileId } = await UploadService.completeFileUpload(
ctx.session.userId,
input.uploadId,
input.contentHmac ?? null,
input.encContentHash,
);
return { file: fileId };
}),
startFileThumbnailUpload: roleProcedure["activeClient"]
.input(
z.object({
file: z.int().positive(),
file: IdSchema,
dekVersion: z.date(),
}),
)
.mutation(async ({ ctx, input }) => {
const { id, path } = await generateThumbnailUploadSession(ctx.session.userId);
try {
await UploadRepo.createThumbnailUploadSession({
id,
type: "thumbnail",
userId: ctx.session.userId,
path,
totalChunks: 1, // Up to 4 MiB
expiresAt: new Date(Date.now() + UPLOADS_EXPIRES),
fileId: input.file,
dekVersion: input.dekVersion,
});
return { uploadId: id };
} catch (e) {
await safeUnlink(path);
if (e instanceof IntegrityError) {
if (e.message === "File not found") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
} else if (e.message === "Invalid DEK version") {
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
}
}
throw e;
}
return await UploadService.startFileThumbnailUpload(
ctx.session.userId,
input.file,
input.dekVersion,
);
}),
completeFileThumbnailUpload: roleProcedure["activeClient"]
@@ -210,43 +91,7 @@ const uploadRouter = router({
}),
)
.mutation(async ({ ctx, input }) => {
const { uploadId } = input;
if (sessionLocks.has(uploadId)) {
throw new TRPCError({ code: "CONFLICT", message: "Completion already in progress" });
} else {
sessionLocks.add(uploadId);
}
try {
const session = await UploadRepo.getUploadSession(uploadId, ctx.session.userId);
if (session?.type !== "thumbnail") {
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid upload id" });
} else if (session.uploadedChunks < session.totalChunks) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Upload not completed" });
}
const oldThumbnailPath = await db.transaction().execute(async (trx) => {
const oldPath = await MediaRepo.updateFileThumbnail(
trx,
ctx.session.userId,
session.fileId,
session.dekVersion,
session.path,
null,
);
await UploadRepo.deleteUploadSession(trx, uploadId);
return oldPath;
});
await safeUnlink(oldThumbnailPath);
} catch (e) {
if (e instanceof IntegrityError && e.message === "Invalid DEK version") {
// DEK rotated after this upload started
throw new TRPCError({ code: "CONFLICT", message: e.message });
}
throw e;
} finally {
sessionLocks.delete(uploadId);
}
await UploadService.completeFileThumbnailUpload(ctx.session.userId, input.uploadId);
}),
});
+4 -9
View File
@@ -1,15 +1,10 @@
import { TRPCError } from "@trpc/server";
import { UserRepo } from "#lib/server/db/index.js";
import { router, roleProcedure } from "../init.server";
import { UserService } from "#lib/server/services/index.js";
import { router, roleProcedure } from "#trpc/init.server.js";
const userRouter = router({
get: roleProcedure["any"].query(async ({ ctx }) => {
const user = await UserRepo.getUser(ctx.session.userId);
if (!user) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" });
}
return { email: user.email, nickname: user.nickname };
const { email, nickname } = await UserService.getUser(ctx.session.userId);
return { email, nickname };
}),
});