mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
강제 로그인 기능 추가
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
confirmText: string;
|
confirmText: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onbeforeclose?: () => void;
|
onbeforeclose?: () => void;
|
||||||
|
oncancel?: () => void;
|
||||||
onConfirmClick: ConfirmHandler;
|
onConfirmClick: ConfirmHandler;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
confirmText,
|
confirmText,
|
||||||
isOpen = $bindable(),
|
isOpen = $bindable(),
|
||||||
onbeforeclose,
|
onbeforeclose,
|
||||||
|
oncancel,
|
||||||
onConfirmClick,
|
onConfirmClick,
|
||||||
title,
|
title,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
@@ -31,6 +33,11 @@
|
|||||||
isOpen = false;
|
isOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancelAction = () => {
|
||||||
|
oncancel?.();
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
const confirmAction = async () => {
|
const confirmAction = async () => {
|
||||||
if ((await onConfirmClick()) !== false) {
|
if ((await onConfirmClick()) !== false) {
|
||||||
closeModal();
|
closeModal();
|
||||||
@@ -38,13 +45,13 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:isOpen onclose={closeModal} class="space-y-4">
|
<Modal bind:isOpen onclose={cancelAction} class="space-y-4">
|
||||||
<div class="flex flex-col gap-y-2 break-keep">
|
<div class="flex flex-col gap-y-2 break-keep">
|
||||||
<p class="text-xl font-bold">{title}</p>
|
<p class="text-xl font-bold">{title}</p>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-x-2">
|
<div class="flex gap-x-2">
|
||||||
<Button color="gray" onclick={closeModal} class="flex-1">{cancelText}</Button>
|
<Button color="gray" onclick={cancelAction} class="flex-1">{cancelText}</Button>
|
||||||
<Button onclick={confirmAction} class="flex-1">{confirmText}</Button>
|
<Button onclick={confirmAction} class="flex-1">{confirmText}</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -46,17 +46,32 @@ export const refreshSession = async (
|
|||||||
return { userId: session.user_id, clientId: session.client_id };
|
return { userId: session.user_id, clientId: session.client_id };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upgradeSession = async (sessionId: string, clientId: number) => {
|
export const upgradeSession = async (
|
||||||
|
userId: number,
|
||||||
|
sessionId: string,
|
||||||
|
clientId: number,
|
||||||
|
force: boolean,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const res = await db
|
await db.transaction().execute(async (trx) => {
|
||||||
.updateTable("session")
|
if (force) {
|
||||||
.set({ client_id: clientId })
|
await trx
|
||||||
.where("id", "=", sessionId)
|
.deleteFrom("session")
|
||||||
.where("client_id", "is", null)
|
.where("id", "!=", sessionId)
|
||||||
.executeTakeFirst();
|
.where("user_id", "=", userId)
|
||||||
if (res.numUpdatedRows === 0n) {
|
.where("client_id", "=", clientId)
|
||||||
throw new IntegrityError("Session not found");
|
.execute();
|
||||||
}
|
}
|
||||||
|
const res = await trx
|
||||||
|
.updateTable("session")
|
||||||
|
.set({ client_id: clientId })
|
||||||
|
.where("id", "=", sessionId)
|
||||||
|
.where("client_id", "is", null)
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (res.numUpdatedRows === 0n) {
|
||||||
|
throw new IntegrityError("Session not found");
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof pg.DatabaseError && e.code === "23505") {
|
if (e instanceof pg.DatabaseError && e.code === "23505") {
|
||||||
throw new IntegrityError("Session already exists");
|
throw new IntegrityError("Session already exists");
|
||||||
|
|||||||
@@ -4,28 +4,29 @@ export const passwordChangeRequest = z.object({
|
|||||||
oldPassword: z.string().trim().nonempty(),
|
oldPassword: z.string().trim().nonempty(),
|
||||||
newPassword: z.string().trim().nonempty(),
|
newPassword: z.string().trim().nonempty(),
|
||||||
});
|
});
|
||||||
export type PasswordChangeRequest = z.infer<typeof passwordChangeRequest>;
|
export type PasswordChangeRequest = z.input<typeof passwordChangeRequest>;
|
||||||
|
|
||||||
export const loginRequest = z.object({
|
export const loginRequest = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string().trim().nonempty(),
|
password: z.string().trim().nonempty(),
|
||||||
});
|
});
|
||||||
export type LoginRequest = z.infer<typeof loginRequest>;
|
export type LoginRequest = z.input<typeof loginRequest>;
|
||||||
|
|
||||||
export const sessionUpgradeRequest = z.object({
|
export const sessionUpgradeRequest = z.object({
|
||||||
encPubKey: z.string().base64().nonempty(),
|
encPubKey: z.string().base64().nonempty(),
|
||||||
sigPubKey: z.string().base64().nonempty(),
|
sigPubKey: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type SessionUpgradeRequest = z.infer<typeof sessionUpgradeRequest>;
|
export type SessionUpgradeRequest = z.input<typeof sessionUpgradeRequest>;
|
||||||
|
|
||||||
export const sessionUpgradeResponse = z.object({
|
export const sessionUpgradeResponse = z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
challenge: z.string().base64().nonempty(),
|
challenge: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type SessionUpgradeResponse = z.infer<typeof sessionUpgradeResponse>;
|
export type SessionUpgradeResponse = z.output<typeof sessionUpgradeResponse>;
|
||||||
|
|
||||||
export const sessionUpgradeVerifyRequest = z.object({
|
export const sessionUpgradeVerifyRequest = z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
answerSig: z.string().base64().nonempty(),
|
answerSig: z.string().base64().nonempty(),
|
||||||
|
force: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
export type SessionUpgradeVerifyRequest = z.infer<typeof sessionUpgradeVerifyRequest>;
|
export type SessionUpgradeVerifyRequest = z.input<typeof sessionUpgradeVerifyRequest>;
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ export const categoryInfoResponse = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
subCategories: z.number().int().positive().array(),
|
subCategories: z.number().int().positive().array(),
|
||||||
});
|
});
|
||||||
export type CategoryInfoResponse = z.infer<typeof categoryInfoResponse>;
|
export type CategoryInfoResponse = z.output<typeof categoryInfoResponse>;
|
||||||
|
|
||||||
export const categoryFileAddRequest = z.object({
|
export const categoryFileAddRequest = z.object({
|
||||||
file: z.number().int().positive(),
|
file: z.number().int().positive(),
|
||||||
});
|
});
|
||||||
export type CategoryFileAddRequest = z.infer<typeof categoryFileAddRequest>;
|
export type CategoryFileAddRequest = z.input<typeof categoryFileAddRequest>;
|
||||||
|
|
||||||
export const categoryFileListResponse = z.object({
|
export const categoryFileListResponse = z.object({
|
||||||
files: z.array(
|
files: z.array(
|
||||||
@@ -30,19 +30,19 @@ export const categoryFileListResponse = z.object({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
export type CategoryFileListResponse = z.infer<typeof categoryFileListResponse>;
|
export type CategoryFileListResponse = z.output<typeof categoryFileListResponse>;
|
||||||
|
|
||||||
export const categoryFileRemoveRequest = z.object({
|
export const categoryFileRemoveRequest = z.object({
|
||||||
file: z.number().int().positive(),
|
file: z.number().int().positive(),
|
||||||
});
|
});
|
||||||
export type CategoryFileRemoveRequest = z.infer<typeof categoryFileRemoveRequest>;
|
export type CategoryFileRemoveRequest = z.input<typeof categoryFileRemoveRequest>;
|
||||||
|
|
||||||
export const categoryRenameRequest = z.object({
|
export const categoryRenameRequest = z.object({
|
||||||
dekVersion: z.string().datetime(),
|
dekVersion: z.string().datetime(),
|
||||||
name: z.string().base64().nonempty(),
|
name: z.string().base64().nonempty(),
|
||||||
nameIv: z.string().base64().nonempty(),
|
nameIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type CategoryRenameRequest = z.infer<typeof categoryRenameRequest>;
|
export type CategoryRenameRequest = z.input<typeof categoryRenameRequest>;
|
||||||
|
|
||||||
export const categoryCreateRequest = z.object({
|
export const categoryCreateRequest = z.object({
|
||||||
parent: categoryIdSchema,
|
parent: categoryIdSchema,
|
||||||
@@ -52,4 +52,4 @@ export const categoryCreateRequest = z.object({
|
|||||||
name: z.string().base64().nonempty(),
|
name: z.string().base64().nonempty(),
|
||||||
nameIv: z.string().base64().nonempty(),
|
nameIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type CategoryCreateRequest = z.infer<typeof categoryCreateRequest>;
|
export type CategoryCreateRequest = z.input<typeof categoryCreateRequest>;
|
||||||
|
|||||||
@@ -8,29 +8,29 @@ export const clientListResponse = z.object({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
export type ClientListResponse = z.infer<typeof clientListResponse>;
|
export type ClientListResponse = z.output<typeof clientListResponse>;
|
||||||
|
|
||||||
export const clientRegisterRequest = z.object({
|
export const clientRegisterRequest = z.object({
|
||||||
encPubKey: z.string().base64().nonempty(),
|
encPubKey: z.string().base64().nonempty(),
|
||||||
sigPubKey: z.string().base64().nonempty(),
|
sigPubKey: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type ClientRegisterRequest = z.infer<typeof clientRegisterRequest>;
|
export type ClientRegisterRequest = z.input<typeof clientRegisterRequest>;
|
||||||
|
|
||||||
export const clientRegisterResponse = z.object({
|
export const clientRegisterResponse = z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
challenge: z.string().base64().nonempty(),
|
challenge: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type ClientRegisterResponse = z.infer<typeof clientRegisterResponse>;
|
export type ClientRegisterResponse = z.output<typeof clientRegisterResponse>;
|
||||||
|
|
||||||
export const clientRegisterVerifyRequest = z.object({
|
export const clientRegisterVerifyRequest = z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
answerSig: z.string().base64().nonempty(),
|
answerSig: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type ClientRegisterVerifyRequest = z.infer<typeof clientRegisterVerifyRequest>;
|
export type ClientRegisterVerifyRequest = z.input<typeof clientRegisterVerifyRequest>;
|
||||||
|
|
||||||
export const clientStatusResponse = z.object({
|
export const clientStatusResponse = z.object({
|
||||||
id: z.number().int().positive(),
|
id: z.number().int().positive(),
|
||||||
state: z.enum(["pending", "active"]),
|
state: z.enum(["pending", "active"]),
|
||||||
isInitialMekNeeded: z.boolean(),
|
isInitialMekNeeded: z.boolean(),
|
||||||
});
|
});
|
||||||
export type ClientStatusResponse = z.infer<typeof clientStatusResponse>;
|
export type ClientStatusResponse = z.output<typeof clientStatusResponse>;
|
||||||
|
|||||||
@@ -16,19 +16,19 @@ export const directoryInfoResponse = z.object({
|
|||||||
subDirectories: z.number().int().positive().array(),
|
subDirectories: z.number().int().positive().array(),
|
||||||
files: z.number().int().positive().array(),
|
files: z.number().int().positive().array(),
|
||||||
});
|
});
|
||||||
export type DirectoryInfoResponse = z.infer<typeof directoryInfoResponse>;
|
export type DirectoryInfoResponse = z.output<typeof directoryInfoResponse>;
|
||||||
|
|
||||||
export const directoryDeleteResponse = z.object({
|
export const directoryDeleteResponse = z.object({
|
||||||
deletedFiles: z.number().int().positive().array(),
|
deletedFiles: z.number().int().positive().array(),
|
||||||
});
|
});
|
||||||
export type DirectoryDeleteResponse = z.infer<typeof directoryDeleteResponse>;
|
export type DirectoryDeleteResponse = z.output<typeof directoryDeleteResponse>;
|
||||||
|
|
||||||
export const directoryRenameRequest = z.object({
|
export const directoryRenameRequest = z.object({
|
||||||
dekVersion: z.string().datetime(),
|
dekVersion: z.string().datetime(),
|
||||||
name: z.string().base64().nonempty(),
|
name: z.string().base64().nonempty(),
|
||||||
nameIv: z.string().base64().nonempty(),
|
nameIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type DirectoryRenameRequest = z.infer<typeof directoryRenameRequest>;
|
export type DirectoryRenameRequest = z.input<typeof directoryRenameRequest>;
|
||||||
|
|
||||||
export const directoryCreateRequest = z.object({
|
export const directoryCreateRequest = z.object({
|
||||||
parent: directoryIdSchema,
|
parent: directoryIdSchema,
|
||||||
@@ -38,4 +38,4 @@ export const directoryCreateRequest = z.object({
|
|||||||
name: z.string().base64().nonempty(),
|
name: z.string().base64().nonempty(),
|
||||||
nameIv: z.string().base64().nonempty(),
|
nameIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type DirectoryCreateRequest = z.infer<typeof directoryCreateRequest>;
|
export type DirectoryCreateRequest = z.input<typeof directoryCreateRequest>;
|
||||||
|
|||||||
@@ -21,42 +21,42 @@ export const fileInfoResponse = z.object({
|
|||||||
lastModifiedAtIv: z.string().base64().nonempty(),
|
lastModifiedAtIv: z.string().base64().nonempty(),
|
||||||
categories: z.number().int().positive().array(),
|
categories: z.number().int().positive().array(),
|
||||||
});
|
});
|
||||||
export type FileInfoResponse = z.infer<typeof fileInfoResponse>;
|
export type FileInfoResponse = z.output<typeof fileInfoResponse>;
|
||||||
|
|
||||||
export const fileRenameRequest = z.object({
|
export const fileRenameRequest = z.object({
|
||||||
dekVersion: z.string().datetime(),
|
dekVersion: z.string().datetime(),
|
||||||
name: z.string().base64().nonempty(),
|
name: z.string().base64().nonempty(),
|
||||||
nameIv: z.string().base64().nonempty(),
|
nameIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type FileRenameRequest = z.infer<typeof fileRenameRequest>;
|
export type FileRenameRequest = z.input<typeof fileRenameRequest>;
|
||||||
|
|
||||||
export const fileThumbnailInfoResponse = z.object({
|
export const fileThumbnailInfoResponse = z.object({
|
||||||
updatedAt: z.string().datetime(),
|
updatedAt: z.string().datetime(),
|
||||||
contentIv: z.string().base64().nonempty(),
|
contentIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type FileThumbnailInfoResponse = z.infer<typeof fileThumbnailInfoResponse>;
|
export type FileThumbnailInfoResponse = z.output<typeof fileThumbnailInfoResponse>;
|
||||||
|
|
||||||
export const fileThumbnailUploadRequest = z.object({
|
export const fileThumbnailUploadRequest = z.object({
|
||||||
dekVersion: z.string().datetime(),
|
dekVersion: z.string().datetime(),
|
||||||
contentIv: z.string().base64().nonempty(),
|
contentIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type FileThumbnailUploadRequest = z.infer<typeof fileThumbnailUploadRequest>;
|
export type FileThumbnailUploadRequest = z.input<typeof fileThumbnailUploadRequest>;
|
||||||
|
|
||||||
export const duplicateFileScanRequest = z.object({
|
export const duplicateFileScanRequest = z.object({
|
||||||
hskVersion: z.number().int().positive(),
|
hskVersion: z.number().int().positive(),
|
||||||
contentHmac: z.string().base64().nonempty(),
|
contentHmac: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type DuplicateFileScanRequest = z.infer<typeof duplicateFileScanRequest>;
|
export type DuplicateFileScanRequest = z.input<typeof duplicateFileScanRequest>;
|
||||||
|
|
||||||
export const duplicateFileScanResponse = z.object({
|
export const duplicateFileScanResponse = z.object({
|
||||||
files: z.number().int().positive().array(),
|
files: z.number().int().positive().array(),
|
||||||
});
|
});
|
||||||
export type DuplicateFileScanResponse = z.infer<typeof duplicateFileScanResponse>;
|
export type DuplicateFileScanResponse = z.output<typeof duplicateFileScanResponse>;
|
||||||
|
|
||||||
export const missingThumbnailFileScanResponse = z.object({
|
export const missingThumbnailFileScanResponse = z.object({
|
||||||
files: z.number().int().positive().array(),
|
files: z.number().int().positive().array(),
|
||||||
});
|
});
|
||||||
export type MissingThumbnailFileScanResponse = z.infer<typeof missingThumbnailFileScanResponse>;
|
export type MissingThumbnailFileScanResponse = z.output<typeof missingThumbnailFileScanResponse>;
|
||||||
|
|
||||||
export const fileUploadRequest = z.object({
|
export const fileUploadRequest = z.object({
|
||||||
parent: directoryIdSchema,
|
parent: directoryIdSchema,
|
||||||
@@ -78,9 +78,9 @@ export const fileUploadRequest = z.object({
|
|||||||
lastModifiedAt: z.string().base64().nonempty(),
|
lastModifiedAt: z.string().base64().nonempty(),
|
||||||
lastModifiedAtIv: z.string().base64().nonempty(),
|
lastModifiedAtIv: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type FileUploadRequest = z.infer<typeof fileUploadRequest>;
|
export type FileUploadRequest = z.input<typeof fileUploadRequest>;
|
||||||
|
|
||||||
export const fileUploadResponse = z.object({
|
export const fileUploadResponse = z.object({
|
||||||
file: z.number().int().positive(),
|
file: z.number().int().positive(),
|
||||||
});
|
});
|
||||||
export type FileUploadResponse = z.infer<typeof fileUploadResponse>;
|
export type FileUploadResponse = z.output<typeof fileUploadResponse>;
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export const hmacSecretListResponse = z.object({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
export type HmacSecretListResponse = z.infer<typeof hmacSecretListResponse>;
|
export type HmacSecretListResponse = z.output<typeof hmacSecretListResponse>;
|
||||||
|
|
||||||
export const initialHmacSecretRegisterRequest = z.object({
|
export const initialHmacSecretRegisterRequest = z.object({
|
||||||
mekVersion: z.number().int().positive(),
|
mekVersion: z.number().int().positive(),
|
||||||
hsk: z.string().base64().nonempty(),
|
hsk: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type InitialHmacSecretRegisterRequest = z.infer<typeof initialHmacSecretRegisterRequest>;
|
export type InitialHmacSecretRegisterRequest = z.input<typeof initialHmacSecretRegisterRequest>;
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export const masterKeyListResponse = z.object({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
export type MasterKeyListResponse = z.infer<typeof masterKeyListResponse>;
|
export type MasterKeyListResponse = z.output<typeof masterKeyListResponse>;
|
||||||
|
|
||||||
export const initialMasterKeyRegisterRequest = z.object({
|
export const initialMasterKeyRegisterRequest = z.object({
|
||||||
mek: z.string().base64().nonempty(),
|
mek: z.string().base64().nonempty(),
|
||||||
mekSig: z.string().base64().nonempty(),
|
mekSig: z.string().base64().nonempty(),
|
||||||
});
|
});
|
||||||
export type InitialMasterKeyRegisterRequest = z.infer<typeof initialMasterKeyRegisterRequest>;
|
export type InitialMasterKeyRegisterRequest = z.input<typeof initialMasterKeyRegisterRequest>;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ export const userInfoResponse = z.object({
|
|||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
nickname: z.string().nonempty(),
|
nickname: z.string().nonempty(),
|
||||||
});
|
});
|
||||||
export type UserInfoResponse = z.infer<typeof userInfoResponse>;
|
export type UserInfoResponse = z.output<typeof userInfoResponse>;
|
||||||
|
|
||||||
export const nicknameChangeRequest = z.object({
|
export const nicknameChangeRequest = z.object({
|
||||||
newNickname: z.string().trim().min(2).max(8),
|
newNickname: z.string().trim().min(2).max(8),
|
||||||
});
|
});
|
||||||
export type NicknameChangeRequest = z.infer<typeof nicknameChangeRequest>;
|
export type NicknameChangeRequest = z.input<typeof nicknameChangeRequest>;
|
||||||
|
|||||||
@@ -87,9 +87,11 @@ export const createSessionUpgradeChallenge = async (
|
|||||||
|
|
||||||
export const verifySessionUpgradeChallenge = async (
|
export const verifySessionUpgradeChallenge = async (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
|
userId: number,
|
||||||
ip: string,
|
ip: string,
|
||||||
challengeId: number,
|
challengeId: number,
|
||||||
answerSig: string,
|
answerSig: string,
|
||||||
|
force: boolean,
|
||||||
) => {
|
) => {
|
||||||
const challenge = await consumeSessionUpgradeChallenge(challengeId, sessionId, ip);
|
const challenge = await consumeSessionUpgradeChallenge(challengeId, sessionId, ip);
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
@@ -106,13 +108,13 @@ export const verifySessionUpgradeChallenge = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await upgradeSession(sessionId, client.id);
|
await upgradeSession(userId, sessionId, client.id, force);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof IntegrityError) {
|
if (e instanceof IntegrityError) {
|
||||||
if (e.message === "Session not found") {
|
if (e.message === "Session not found") {
|
||||||
error(500, "Invalid challenge answer");
|
error(500, "Invalid challenge answer");
|
||||||
} else if (e.message === "Session already exists") {
|
} else if (!force && e.message === "Session already exists") {
|
||||||
error(403, "Already logged in");
|
error(409, "Already logged in");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ export const requestSessionUpgrade = async (
|
|||||||
decryptKey: CryptoKey,
|
decryptKey: CryptoKey,
|
||||||
verifyKeyBase64: string,
|
verifyKeyBase64: string,
|
||||||
signKey: CryptoKey,
|
signKey: CryptoKey,
|
||||||
|
force = false,
|
||||||
) => {
|
) => {
|
||||||
let res = await callPostApi<SessionUpgradeRequest>("/api/auth/upgradeSession", {
|
let res = await callPostApi<SessionUpgradeRequest>("/api/auth/upgradeSession", {
|
||||||
encPubKey: encryptKeyBase64,
|
encPubKey: encryptKeyBase64,
|
||||||
sigPubKey: verifyKeyBase64,
|
sigPubKey: verifyKeyBase64,
|
||||||
});
|
});
|
||||||
if (!res.ok) return false;
|
if (res.status === 403) return [false, "Unregistered client"] as const;
|
||||||
|
else if (!res.ok) return [false] as const;
|
||||||
|
|
||||||
const { id, challenge }: SessionUpgradeResponse = await res.json();
|
const { id, challenge }: SessionUpgradeResponse = await res.json();
|
||||||
const answer = await decryptChallenge(challenge, decryptKey);
|
const answer = await decryptChallenge(challenge, decryptKey);
|
||||||
@@ -25,6 +27,13 @@ export const requestSessionUpgrade = async (
|
|||||||
res = await callPostApi<SessionUpgradeVerifyRequest>("/api/auth/upgradeSession/verify", {
|
res = await callPostApi<SessionUpgradeVerifyRequest>("/api/auth/upgradeSession/verify", {
|
||||||
id,
|
id,
|
||||||
answerSig: encodeToBase64(answerSig),
|
answerSig: encodeToBase64(answerSig),
|
||||||
|
force,
|
||||||
});
|
});
|
||||||
|
if (res.status === 409) return [false, "Already logged in"] as const;
|
||||||
|
else return [res.ok] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestLogout = async () => {
|
||||||
|
const res = await callPostApi("/api/auth/logout");
|
||||||
return res.ok;
|
return res.ok;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,18 @@
|
|||||||
import { BottomDiv, Button, FullscreenDiv, TextButton, TextInput } from "$lib/components/atoms";
|
import { BottomDiv, Button, FullscreenDiv, TextButton, TextInput } from "$lib/components/atoms";
|
||||||
import { TitledDiv } from "$lib/components/molecules";
|
import { TitledDiv } from "$lib/components/molecules";
|
||||||
import { clientKeyStore, masterKeyStore } from "$lib/stores";
|
import { clientKeyStore, masterKeyStore } from "$lib/stores";
|
||||||
import { requestLogin, requestSessionUpgrade, requestMasterKeyDownload } from "./service";
|
import ForceLoginModal from "./ForceLoginModal.svelte";
|
||||||
|
import {
|
||||||
|
requestLogout,
|
||||||
|
requestLogin,
|
||||||
|
requestSessionUpgrade,
|
||||||
|
requestMasterKeyDownload,
|
||||||
|
} from "./service";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
|
let isForceLoginModalOpen = $state(false);
|
||||||
|
|
||||||
let email = $state("");
|
let email = $state("");
|
||||||
let password = $state("");
|
let password = $state("");
|
||||||
|
|
||||||
@@ -14,6 +22,32 @@
|
|||||||
return await goto(`${url}?redirect=${encodeURIComponent(data.redirectPath)}`);
|
return await goto(`${url}?redirect=${encodeURIComponent(data.redirectPath)}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const upgradeSession = async (force: boolean) => {
|
||||||
|
try {
|
||||||
|
const [upgradeRes, upgradeError] = await requestSessionUpgrade($clientKeyStore!, force);
|
||||||
|
if (!force && upgradeError === "Already logged in") {
|
||||||
|
isForceLoginModalOpen = true;
|
||||||
|
return;
|
||||||
|
} else if (!upgradeRes) {
|
||||||
|
throw new Error("Failed to upgrade session");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Multi-user support
|
||||||
|
|
||||||
|
if (
|
||||||
|
$masterKeyStore ||
|
||||||
|
(await requestMasterKeyDownload($clientKeyStore!.decryptKey, $clientKeyStore!.verifyKey))
|
||||||
|
) {
|
||||||
|
await goto(data.redirectPath);
|
||||||
|
} else {
|
||||||
|
await redirect("/client/pending");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// TODO
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
// TODO: Validation
|
// TODO: Validation
|
||||||
|
|
||||||
@@ -22,19 +56,7 @@
|
|||||||
|
|
||||||
if (!$clientKeyStore) return await redirect("/key/generate");
|
if (!$clientKeyStore) return await redirect("/key/generate");
|
||||||
|
|
||||||
if (!(await requestSessionUpgrade($clientKeyStore)))
|
await upgradeSession(false);
|
||||||
throw new Error("Failed to upgrade session");
|
|
||||||
|
|
||||||
// TODO: Multi-user support
|
|
||||||
|
|
||||||
if (
|
|
||||||
$masterKeyStore ||
|
|
||||||
(await requestMasterKeyDownload($clientKeyStore.decryptKey, $clientKeyStore.verifyKey))
|
|
||||||
) {
|
|
||||||
await goto(data.redirectPath);
|
|
||||||
} else {
|
|
||||||
await redirect("/client/pending");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: Alert
|
// TODO: Alert
|
||||||
throw e;
|
throw e;
|
||||||
@@ -63,3 +85,9 @@
|
|||||||
<TextButton>계정이 없어요</TextButton>
|
<TextButton>계정이 없어요</TextButton>
|
||||||
</BottomDiv>
|
</BottomDiv>
|
||||||
</FullscreenDiv>
|
</FullscreenDiv>
|
||||||
|
|
||||||
|
<ForceLoginModal
|
||||||
|
bind:isOpen={isForceLoginModalOpen}
|
||||||
|
oncancel={requestLogout}
|
||||||
|
onLoginClick={() => upgradeSession(true)}
|
||||||
|
/>
|
||||||
|
|||||||
22
src/routes/(fullscreen)/auth/login/ForceLoginModal.svelte
Normal file
22
src/routes/(fullscreen)/auth/login/ForceLoginModal.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ActionModal } from "$lib/components/molecules";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
oncancel: () => void;
|
||||||
|
onLoginClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { isOpen = $bindable(), oncancel, onLoginClick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionModal
|
||||||
|
bind:isOpen
|
||||||
|
title="다른 디바이스에 이미 로그인되어 있어요."
|
||||||
|
cancelText="아니요"
|
||||||
|
{oncancel}
|
||||||
|
confirmText="네"
|
||||||
|
onConfirmClick={onLoginClick}
|
||||||
|
>
|
||||||
|
<p>다른 디바이스에서는 로그아웃하고, 이 디바이스에서 로그인할까요?</p>
|
||||||
|
</ActionModal>
|
||||||
@@ -5,6 +5,7 @@ import { requestSessionUpgrade as requestSessionUpgradeInternal } from "$lib/ser
|
|||||||
import { requestClientRegistration } from "$lib/services/key";
|
import { requestClientRegistration } from "$lib/services/key";
|
||||||
import type { ClientKeys } from "$lib/stores";
|
import type { ClientKeys } from "$lib/stores";
|
||||||
|
|
||||||
|
export { requestLogout } from "$lib/services/auth";
|
||||||
export { requestMasterKeyDownload } from "$lib/services/key";
|
export { requestMasterKeyDownload } from "$lib/services/key";
|
||||||
|
|
||||||
export const requestLogin = async (email: string, password: string) => {
|
export const requestLogin = async (email: string, password: string) => {
|
||||||
@@ -12,26 +13,33 @@ export const requestLogin = async (email: string, password: string) => {
|
|||||||
return res.ok;
|
return res.ok;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestSessionUpgrade = async ({
|
export const requestSessionUpgrade = async (
|
||||||
encryptKey,
|
{ encryptKey, decryptKey, signKey, verifyKey }: ClientKeys,
|
||||||
decryptKey,
|
force: boolean,
|
||||||
signKey,
|
) => {
|
||||||
verifyKey,
|
|
||||||
}: ClientKeys) => {
|
|
||||||
const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey);
|
const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey);
|
||||||
const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey);
|
const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey);
|
||||||
if (await requestSessionUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) {
|
const [res, error] = await requestSessionUpgradeInternal(
|
||||||
return true;
|
encryptKeyBase64,
|
||||||
|
decryptKey,
|
||||||
|
verifyKeyBase64,
|
||||||
|
signKey,
|
||||||
|
force,
|
||||||
|
);
|
||||||
|
if (error === undefined) return [res] as const;
|
||||||
|
|
||||||
|
if (
|
||||||
|
error === "Unregistered client" &&
|
||||||
|
!(await requestClientRegistration(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey))
|
||||||
|
) {
|
||||||
|
return [false] as const;
|
||||||
|
} else if (error === "Already logged in") {
|
||||||
|
return [false, force ? undefined : error] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await requestClientRegistration(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) {
|
return [
|
||||||
return await requestSessionUpgradeInternal(
|
(
|
||||||
encryptKeyBase64,
|
await requestSessionUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)
|
||||||
decryptKey,
|
)[0],
|
||||||
verifyKeyBase64,
|
] as const;
|
||||||
signKey,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,12 +59,14 @@
|
|||||||
await storeClientKeys($clientKeyStore);
|
await storeClientKeys($clientKeyStore);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!(await requestSessionUpgrade(
|
!(
|
||||||
data.encryptKeyBase64,
|
await requestSessionUpgrade(
|
||||||
$clientKeyStore.decryptKey,
|
data.encryptKeyBase64,
|
||||||
data.verifyKeyBase64,
|
$clientKeyStore.decryptKey,
|
||||||
$clientKeyStore.signKey,
|
data.verifyKeyBase64,
|
||||||
))
|
$clientKeyStore.signKey,
|
||||||
|
)
|
||||||
|
)[0]
|
||||||
)
|
)
|
||||||
throw new Error("Failed to upgrade session");
|
throw new Error("Failed to upgrade session");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1 @@
|
|||||||
import { callPostApi } from "$lib/hooks";
|
export { requestLogout } from "$lib/services/auth";
|
||||||
|
|
||||||
export const requestLogout = async () => {
|
|
||||||
const res = await callPostApi("/api/auth/logout");
|
|
||||||
return res.ok;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { verifySessionUpgradeChallenge } from "$lib/server/services/auth";
|
|||||||
import type { RequestHandler } from "./$types";
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ locals, request }) => {
|
export const POST: RequestHandler = async ({ locals, request }) => {
|
||||||
const { sessionId } = await authorize(locals, "notClient");
|
const { sessionId, userId } = await authorize(locals, "notClient");
|
||||||
|
|
||||||
const zodRes = sessionUpgradeVerifyRequest.safeParse(await request.json());
|
const zodRes = sessionUpgradeVerifyRequest.safeParse(await request.json());
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
if (!zodRes.success) error(400, "Invalid request body");
|
||||||
const { id, answerSig } = zodRes.data;
|
const { id, answerSig, force } = zodRes.data;
|
||||||
|
|
||||||
await verifySessionUpgradeChallenge(sessionId, locals.ip, id, answerSig);
|
await verifySessionUpgradeChallenge(sessionId, userId, locals.ip, id, answerSig, force);
|
||||||
return text("Session upgraded", { headers: { "Content-Type": "text/plain" } });
|
return text("Session upgraded", { headers: { "Content-Type": "text/plain" } });
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user