강제 로그인 기능 추가

This commit is contained in:
static
2025-07-11 23:15:35 +09:00
parent fa8c163347
commit c47885d571
18 changed files with 187 additions and 98 deletions

View File

@@ -3,10 +3,18 @@
import { BottomDiv, Button, FullscreenDiv, TextButton, TextInput } from "$lib/components/atoms";
import { TitledDiv } from "$lib/components/molecules";
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 isForceLoginModalOpen = $state(false);
let email = $state("");
let password = $state("");
@@ -14,6 +22,32 @@
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 () => {
// TODO: Validation
@@ -22,19 +56,7 @@
if (!$clientKeyStore) return await redirect("/key/generate");
if (!(await requestSessionUpgrade($clientKeyStore)))
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");
}
await upgradeSession(false);
} catch (e) {
// TODO: Alert
throw e;
@@ -63,3 +85,9 @@
<TextButton>계정이 없어요</TextButton>
</BottomDiv>
</FullscreenDiv>
<ForceLoginModal
bind:isOpen={isForceLoginModalOpen}
oncancel={requestLogout}
onLoginClick={() => upgradeSession(true)}
/>

View 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>

View File

@@ -5,6 +5,7 @@ import { requestSessionUpgrade as requestSessionUpgradeInternal } from "$lib/ser
import { requestClientRegistration } from "$lib/services/key";
import type { ClientKeys } from "$lib/stores";
export { requestLogout } from "$lib/services/auth";
export { requestMasterKeyDownload } from "$lib/services/key";
export const requestLogin = async (email: string, password: string) => {
@@ -12,26 +13,33 @@ export const requestLogin = async (email: string, password: string) => {
return res.ok;
};
export const requestSessionUpgrade = async ({
encryptKey,
decryptKey,
signKey,
verifyKey,
}: ClientKeys) => {
export const requestSessionUpgrade = async (
{ encryptKey, decryptKey, signKey, verifyKey }: ClientKeys,
force: boolean,
) => {
const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey);
const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey);
if (await requestSessionUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) {
return true;
const [res, error] = await requestSessionUpgradeInternal(
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 await requestSessionUpgradeInternal(
encryptKeyBase64,
decryptKey,
verifyKeyBase64,
signKey,
);
} else {
return false;
}
return [
(
await requestSessionUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)
)[0],
] as const;
};

View File

@@ -59,12 +59,14 @@
await storeClientKeys($clientKeyStore);
if (
!(await requestSessionUpgrade(
data.encryptKeyBase64,
$clientKeyStore.decryptKey,
data.verifyKeyBase64,
$clientKeyStore.signKey,
))
!(
await requestSessionUpgrade(
data.encryptKeyBase64,
$clientKeyStore.decryptKey,
data.verifyKeyBase64,
$clientKeyStore.signKey,
)
)[0]
)
throw new Error("Failed to upgrade session");

View File

@@ -1,6 +1 @@
import { callPostApi } from "$lib/hooks";
export const requestLogout = async () => {
const res = await callPostApi("/api/auth/logout");
return res.ok;
};
export { requestLogout } from "$lib/services/auth";

View File

@@ -5,12 +5,12 @@ import { verifySessionUpgradeChallenge } from "$lib/server/services/auth";
import type { RequestHandler } from "./$types";
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());
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" } });
};