mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
DB에 동시적으로 접근하더라도 데이터 무결성이 깨지지 않도록 DB 접근 코드 수정
This commit is contained in:
@@ -4,9 +4,10 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { getClient, getClientByPubKeys, getUserClient } from "$lib/server/db/client";
|
||||
import { getUserByEmail } from "$lib/server/db/user";
|
||||
import env from "$lib/server/loadenv";
|
||||
import { IntegrityError } from "$lib/server/db/error";
|
||||
import {
|
||||
getRefreshToken,
|
||||
registerRefreshToken,
|
||||
getRefreshToken,
|
||||
rotateRefreshToken,
|
||||
upgradeRefreshToken,
|
||||
revokeRefreshToken,
|
||||
@@ -29,10 +30,15 @@ const issueRefreshToken = async (userId: number, clientId?: number) => {
|
||||
const jti = uuidv4();
|
||||
const token = issueToken({ type: "refresh", jti });
|
||||
|
||||
if (!(await registerRefreshToken(userId, clientId ?? null, jti))) {
|
||||
error(403, "Already logged in");
|
||||
try {
|
||||
await registerRefreshToken(userId, clientId ?? null, jti);
|
||||
return token;
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "Refresh token already registered") {
|
||||
error(409, "Already logged in");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
export const login = async (email: string, password: string) => {
|
||||
@@ -57,7 +63,7 @@ const verifyRefreshToken = async (refreshToken: string) => {
|
||||
|
||||
const tokenData = await getRefreshToken(tokenPayload.jti);
|
||||
if (!tokenData) {
|
||||
error(500, "Refresh token not found");
|
||||
error(500, "Invalid refresh token");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -76,13 +82,18 @@ export const refreshToken = async (refreshToken: string) => {
|
||||
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
|
||||
const newJti = uuidv4();
|
||||
|
||||
if (!(await rotateRefreshToken(oldJti, newJti))) {
|
||||
error(500, "Refresh token not found");
|
||||
try {
|
||||
await rotateRefreshToken(oldJti, newJti);
|
||||
return {
|
||||
accessToken: issueAccessToken(userId, clientId),
|
||||
refreshToken: issueToken({ type: "refresh", jti: newJti }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "Refresh token not found") {
|
||||
error(500, "Invalid refresh token");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return {
|
||||
accessToken: issueAccessToken(userId, clientId),
|
||||
refreshToken: issueToken({ type: "refresh", jti: newJti }),
|
||||
};
|
||||
};
|
||||
|
||||
const expiresAt = () => new Date(Date.now() + env.challenge.tokenUpgradeExp);
|
||||
@@ -120,7 +131,7 @@ export const createTokenUpgradeChallenge = async (
|
||||
if (!client) {
|
||||
error(401, "Invalid public key(s)");
|
||||
} else if (!userClient || userClient.state === "challenging") {
|
||||
error(401, "Unregistered client");
|
||||
error(403, "Unregistered client");
|
||||
}
|
||||
|
||||
return { challenge: await createChallenge(ip, jti, client.id, encPubKey) };
|
||||
@@ -139,26 +150,31 @@ export const upgradeToken = async (
|
||||
|
||||
const challenge = await getTokenUpgradeChallenge(answer, ip);
|
||||
if (!challenge) {
|
||||
error(401, "Invalid challenge answer");
|
||||
error(403, "Invalid challenge answer");
|
||||
} else if (challenge.refreshTokenId !== oldJti) {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
|
||||
await markTokenUpgradeChallengeAsUsed(challenge.id);
|
||||
|
||||
const client = await getClient(challenge.clientId);
|
||||
if (!client) {
|
||||
error(500, "Invalid challenge answer");
|
||||
} else if (!verifySignature(Buffer.from(answer, "base64"), answerSig, client.sigPubKey)) {
|
||||
error(401, "Invalid challenge answer signature");
|
||||
error(403, "Invalid challenge answer signature");
|
||||
}
|
||||
|
||||
await markTokenUpgradeChallengeAsUsed(challenge.id);
|
||||
|
||||
const newJti = uuidv4();
|
||||
if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) {
|
||||
error(500, "Refresh token not found");
|
||||
try {
|
||||
const newJti = uuidv4();
|
||||
await upgradeRefreshToken(oldJti, newJti, client.id);
|
||||
return {
|
||||
accessToken: issueAccessToken(userId, client.id),
|
||||
refreshToken: issueToken({ type: "refresh", jti: newJti }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "Refresh token not found") {
|
||||
error(500, "Invalid refresh token");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return {
|
||||
accessToken: issueAccessToken(userId, client.id),
|
||||
refreshToken: issueToken({ type: "refresh", jti: newJti }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
createClient,
|
||||
getClient,
|
||||
getClientByPubKeys,
|
||||
countClientByPubKey,
|
||||
createUserClient,
|
||||
getAllUserClients,
|
||||
getUserClient,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
getUserClientChallenge,
|
||||
markUserClientChallengeAsUsed,
|
||||
} from "$lib/server/db/client";
|
||||
import { IntegrityError } from "$lib/server/db/error";
|
||||
import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto";
|
||||
import { isInitialMekNeeded } from "$lib/server/modules/mek";
|
||||
import env from "$lib/server/loadenv";
|
||||
@@ -29,8 +29,8 @@ export const getUserClientList = async (userId: number) => {
|
||||
const expiresAt = () => new Date(Date.now() + env.challenge.userClientExp);
|
||||
|
||||
const createUserClientChallenge = async (
|
||||
userId: number,
|
||||
ip: string,
|
||||
userId: number,
|
||||
clientId: number,
|
||||
encPubKey: string,
|
||||
) => {
|
||||
@@ -45,33 +45,59 @@ export const registerUserClient = async (
|
||||
encPubKey: string,
|
||||
sigPubKey: string,
|
||||
) => {
|
||||
let clientId;
|
||||
|
||||
const client = await getClientByPubKeys(encPubKey, sigPubKey);
|
||||
if (client) {
|
||||
const userClient = await getUserClient(userId, client.id);
|
||||
if (userClient) {
|
||||
error(409, "Client already registered");
|
||||
try {
|
||||
await createUserClient(userId, client.id);
|
||||
return { challenge: await createUserClientChallenge(ip, userId, client.id, encPubKey) };
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "User client already exists") {
|
||||
error(409, "Client already registered");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
await createUserClient(userId, client.id);
|
||||
clientId = client.id;
|
||||
} else {
|
||||
if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) {
|
||||
if (encPubKey === sigPubKey) {
|
||||
error(400, "Same public keys");
|
||||
} else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) {
|
||||
error(400, "Invalid public key(s)");
|
||||
} else if (encPubKey === sigPubKey) {
|
||||
error(400, "Public keys must be different");
|
||||
} else if (
|
||||
(await countClientByPubKey(encPubKey)) > 0 ||
|
||||
(await countClientByPubKey(sigPubKey)) > 0
|
||||
) {
|
||||
error(409, "Public key(s) already registered");
|
||||
}
|
||||
|
||||
clientId = await createClient(encPubKey, sigPubKey, userId);
|
||||
try {
|
||||
const clientId = await createClient(encPubKey, sigPubKey, userId);
|
||||
return { challenge: await createUserClientChallenge(ip, userId, clientId, encPubKey) };
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "Public key(s) already registered") {
|
||||
error(409, "Public key(s) already used");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyUserClient = async (
|
||||
userId: number,
|
||||
ip: string,
|
||||
answer: string,
|
||||
answerSig: string,
|
||||
) => {
|
||||
const challenge = await getUserClientChallenge(answer, ip);
|
||||
if (!challenge) {
|
||||
error(403, "Invalid challenge answer");
|
||||
} else if (challenge.userId !== userId) {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
|
||||
return { challenge: await createUserClientChallenge(userId, ip, clientId, encPubKey) };
|
||||
await markUserClientChallengeAsUsed(challenge.id);
|
||||
|
||||
const client = await getClient(challenge.clientId);
|
||||
if (!client) {
|
||||
error(500, "Invalid challenge answer");
|
||||
} else if (!verifySignature(Buffer.from(answer, "base64"), answerSig, client.sigPubKey)) {
|
||||
error(403, "Invalid challenge answer signature");
|
||||
}
|
||||
|
||||
await setUserClientStateToPending(userId, challenge.clientId);
|
||||
};
|
||||
|
||||
export const getUserClientStatus = async (userId: number, clientId: number) => {
|
||||
@@ -85,27 +111,3 @@ export const getUserClientStatus = async (userId: number, clientId: number) => {
|
||||
isInitialMekNeeded: await isInitialMekNeeded(userId),
|
||||
};
|
||||
};
|
||||
|
||||
export const verifyUserClient = async (
|
||||
userId: number,
|
||||
ip: string,
|
||||
answer: string,
|
||||
answerSig: string,
|
||||
) => {
|
||||
const challenge = await getUserClientChallenge(answer, ip);
|
||||
if (!challenge) {
|
||||
error(401, "Invalid challenge answer");
|
||||
} else if (challenge.userId !== userId) {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
|
||||
const client = await getClient(challenge.clientId);
|
||||
if (!client) {
|
||||
error(500, "Invalid challenge answer");
|
||||
} else if (!verifySignature(Buffer.from(answer, "base64"), answerSig, client.sigPubKey)) {
|
||||
error(401, "Invalid challenge answer signature");
|
||||
}
|
||||
|
||||
await markUserClientChallengeAsUsed(challenge.id);
|
||||
await setUserClientStateToPending(userId, challenge.clientId);
|
||||
};
|
||||
|
||||
@@ -1,44 +1,15 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { unlink } from "fs/promises";
|
||||
import { IntegrityError } from "$lib/server/db/error";
|
||||
import {
|
||||
registerDirectory,
|
||||
getAllDirectoriesByParent,
|
||||
registerNewDirectory,
|
||||
getDirectory,
|
||||
setDirectoryEncName,
|
||||
unregisterDirectory,
|
||||
getAllFilesByParent,
|
||||
type NewDirectoryParams,
|
||||
} from "$lib/server/db/file";
|
||||
import { getActiveMekVersion } from "$lib/server/db/mek";
|
||||
|
||||
export const deleteDirectory = async (userId: number, directoryId: number) => {
|
||||
const directory = await getDirectory(userId, directoryId);
|
||||
if (!directory) {
|
||||
error(404, "Invalid directory id");
|
||||
}
|
||||
|
||||
const filePaths = await unregisterDirectory(userId, directoryId);
|
||||
filePaths.map((path) => unlink(path)); // Intended
|
||||
};
|
||||
|
||||
export const renameDirectory = async (
|
||||
userId: number,
|
||||
directoryId: number,
|
||||
dekVersion: Date,
|
||||
newEncName: string,
|
||||
newEncNameIv: string,
|
||||
) => {
|
||||
const directory = await getDirectory(userId, directoryId);
|
||||
if (!directory) {
|
||||
error(404, "Invalid directory id");
|
||||
} else if (directory.dekVersion.getTime() !== dekVersion.getTime()) {
|
||||
error(400, "Invalid DEK version");
|
||||
}
|
||||
|
||||
if (!(await setDirectoryEncName(userId, directoryId, dekVersion, newEncName, newEncNameIv))) {
|
||||
error(500, "Invalid directory id or DEK version");
|
||||
}
|
||||
};
|
||||
|
||||
export const getDirectoryInformation = async (userId: number, directoryId: "root" | number) => {
|
||||
const directory = directoryId !== "root" ? await getDirectory(userId, directoryId) : undefined;
|
||||
@@ -62,19 +33,52 @@ export const getDirectoryInformation = async (userId: number, directoryId: "root
|
||||
};
|
||||
};
|
||||
|
||||
export const createDirectory = async (params: NewDirectoryParams) => {
|
||||
const activeMekVersion = await getActiveMekVersion(params.userId);
|
||||
if (activeMekVersion === null) {
|
||||
error(500, "Invalid MEK version");
|
||||
} else if (activeMekVersion !== params.mekVersion) {
|
||||
error(400, "Invalid MEK version");
|
||||
export const deleteDirectory = async (userId: number, directoryId: number) => {
|
||||
try {
|
||||
const filePaths = await unregisterDirectory(userId, directoryId);
|
||||
filePaths.map((path) => unlink(path)); // Intended
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "Directory not found") {
|
||||
error(404, "Invalid directory id");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const renameDirectory = async (
|
||||
userId: number,
|
||||
directoryId: number,
|
||||
dekVersion: Date,
|
||||
newEncName: string,
|
||||
newEncNameIv: string,
|
||||
) => {
|
||||
try {
|
||||
await setDirectoryEncName(userId, directoryId, dekVersion, newEncName, newEncNameIv);
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError) {
|
||||
if (e.message === "Directory not found") {
|
||||
error(404, "Invalid directory id");
|
||||
} else if (e.message === "Invalid DEK version") {
|
||||
error(400, "Invalid DEK version");
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const createDirectory = async (params: NewDirectoryParams) => {
|
||||
const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
|
||||
const oneMinuteLater = new Date(Date.now() + 60 * 1000);
|
||||
if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) {
|
||||
error(400, "Invalid DEK version");
|
||||
}
|
||||
|
||||
await registerNewDirectory(params);
|
||||
try {
|
||||
await registerDirectory(params);
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "Inactive MEK version") {
|
||||
error(400, "Invalid MEK version");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,77 +1,19 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { createReadStream, createWriteStream, ReadStream, WriteStream } from "fs";
|
||||
import { createReadStream, createWriteStream } from "fs";
|
||||
import { mkdir, stat, unlink } from "fs/promises";
|
||||
import { dirname } from "path";
|
||||
import { Readable, Writable } from "stream";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { IntegrityError } from "$lib/server/db/error";
|
||||
import {
|
||||
registerNewFile,
|
||||
registerFile,
|
||||
getFile,
|
||||
setFileEncName,
|
||||
unregisterFile,
|
||||
type NewFileParams,
|
||||
} from "$lib/server/db/file";
|
||||
import { getActiveMekVersion } from "$lib/server/db/mek";
|
||||
import env from "$lib/server/loadenv";
|
||||
|
||||
export const deleteFile = async (userId: number, fileId: number) => {
|
||||
const file = await getFile(userId, fileId);
|
||||
if (!file) {
|
||||
error(404, "Invalid file id");
|
||||
}
|
||||
|
||||
const path = await unregisterFile(userId, fileId);
|
||||
if (!path) {
|
||||
error(500, "Invalid file id");
|
||||
}
|
||||
|
||||
unlink(path); // Intended
|
||||
};
|
||||
|
||||
const convertToReadableStream = (readStream: ReadStream) => {
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start: (controller) => {
|
||||
readStream.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk as Buffer)));
|
||||
readStream.on("end", () => controller.close());
|
||||
readStream.on("error", (e) => controller.error(e));
|
||||
},
|
||||
cancel: () => {
|
||||
readStream.destroy();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getFileStream = async (userId: number, fileId: number) => {
|
||||
const file = await getFile(userId, fileId);
|
||||
if (!file) {
|
||||
error(404, "Invalid file id");
|
||||
}
|
||||
|
||||
const { size } = await stat(file.path);
|
||||
return {
|
||||
encContentStream: convertToReadableStream(createReadStream(file.path)),
|
||||
encContentSize: size,
|
||||
};
|
||||
};
|
||||
|
||||
export const renameFile = async (
|
||||
userId: number,
|
||||
fileId: number,
|
||||
dekVersion: Date,
|
||||
newEncName: string,
|
||||
newEncNameIv: string,
|
||||
) => {
|
||||
const file = await getFile(userId, fileId);
|
||||
if (!file) {
|
||||
error(404, "Invalid file id");
|
||||
} else if (file.dekVersion.getTime() !== dekVersion.getTime()) {
|
||||
error(400, "Invalid DEK version");
|
||||
}
|
||||
|
||||
if (!(await setFileEncName(userId, fileId, dekVersion, newEncName, newEncNameIv))) {
|
||||
error(500, "Invalid file id or DEK version");
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileInformation = async (userId: number, fileId: number) => {
|
||||
const file = await getFile(userId, fileId);
|
||||
if (!file) {
|
||||
@@ -89,20 +31,50 @@ export const getFileInformation = async (userId: number, fileId: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
const convertToWritableStream = (writeStream: WriteStream) => {
|
||||
return new WritableStream<Uint8Array>({
|
||||
write: (chunk) =>
|
||||
new Promise((resolve, reject) => {
|
||||
writeStream.write(chunk, (e) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}),
|
||||
close: () => new Promise((resolve) => writeStream.end(resolve)),
|
||||
});
|
||||
export const deleteFile = async (userId: number, fileId: number) => {
|
||||
try {
|
||||
const filePath = await unregisterFile(userId, fileId);
|
||||
unlink(filePath); // Intended
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "File not found") {
|
||||
error(404, "Invalid file id");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileStream = async (userId: number, fileId: number) => {
|
||||
const file = await getFile(userId, fileId);
|
||||
if (!file) {
|
||||
error(404, "Invalid file id");
|
||||
}
|
||||
|
||||
const { size } = await stat(file.path);
|
||||
return {
|
||||
encContentStream: Readable.toWeb(createReadStream(file.path)),
|
||||
encContentSize: size,
|
||||
};
|
||||
};
|
||||
|
||||
export const renameFile = async (
|
||||
userId: number,
|
||||
fileId: number,
|
||||
dekVersion: Date,
|
||||
newEncName: string,
|
||||
newEncNameIv: string,
|
||||
) => {
|
||||
try {
|
||||
await setFileEncName(userId, fileId, dekVersion, newEncName, newEncNameIv);
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError) {
|
||||
if (e.message === "File not found") {
|
||||
error(404, "Invalid file id");
|
||||
} else if (e.message === "Invalid DEK version") {
|
||||
error(400, "Invalid DEK version");
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const safeUnlink = async (path: string) => {
|
||||
@@ -113,13 +85,6 @@ export const uploadFile = async (
|
||||
params: Omit<NewFileParams, "path">,
|
||||
encContentStream: ReadableStream<Uint8Array>,
|
||||
) => {
|
||||
const activeMekVersion = await getActiveMekVersion(params.userId);
|
||||
if (activeMekVersion === null) {
|
||||
error(500, "Invalid MEK version");
|
||||
} else if (activeMekVersion !== params.mekVersion) {
|
||||
error(400, "Invalid MEK version");
|
||||
}
|
||||
|
||||
const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
|
||||
const oneMinuteLater = new Date(Date.now() + 60 * 1000);
|
||||
if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) {
|
||||
@@ -131,14 +96,20 @@ export const uploadFile = async (
|
||||
|
||||
try {
|
||||
await encContentStream.pipeTo(
|
||||
convertToWritableStream(createWriteStream(path, { flags: "wx", mode: 0o600 })),
|
||||
Writable.toWeb(createWriteStream(path, { flags: "wx", mode: 0o600 })),
|
||||
);
|
||||
await registerNewFile({
|
||||
await registerFile({
|
||||
...params,
|
||||
path,
|
||||
});
|
||||
} catch (e) {
|
||||
await safeUnlink(path);
|
||||
|
||||
if (e instanceof IntegrityError) {
|
||||
if (e.message === "Inactive MEK version") {
|
||||
error(400, "Invalid MEK version");
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { setUserClientStateToActive } from "$lib/server/db/client";
|
||||
import { IntegrityError } from "$lib/server/db/error";
|
||||
import { registerInitialMek, getAllValidClientMeks } from "$lib/server/db/mek";
|
||||
import { isInitialMekNeeded, verifyClientEncMekSig } from "$lib/server/modules/mek";
|
||||
import { verifyClientEncMekSig } from "$lib/server/modules/mek";
|
||||
|
||||
export const getClientMekList = async (userId: number, clientId: number) => {
|
||||
const clientMeks = await getAllValidClientMeks(userId, clientId);
|
||||
@@ -21,12 +22,17 @@ export const registerInitialActiveMek = async (
|
||||
encMek: string,
|
||||
encMekSig: string,
|
||||
) => {
|
||||
if (!(await isInitialMekNeeded(userId))) {
|
||||
error(409, "Initial MEK already registered");
|
||||
} else if (!(await verifyClientEncMekSig(userId, createdBy, 1, encMek, encMekSig))) {
|
||||
if (!(await verifyClientEncMekSig(userId, createdBy, 1, encMek, encMekSig))) {
|
||||
error(400, "Invalid signature");
|
||||
}
|
||||
|
||||
await registerInitialMek(userId, createdBy, encMek, encMekSig);
|
||||
await setUserClientStateToActive(userId, createdBy);
|
||||
try {
|
||||
await registerInitialMek(userId, createdBy, encMek, encMekSig);
|
||||
await setUserClientStateToActive(userId, createdBy);
|
||||
} catch (e) {
|
||||
if (e instanceof IntegrityError && e.message === "MEK already registered") {
|
||||
error(409, "Initial MEK already registered");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user