강제 로그인 기능 추가

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

@@ -12,6 +12,7 @@
confirmText: string;
isOpen: boolean;
onbeforeclose?: () => void;
oncancel?: () => void;
onConfirmClick: ConfirmHandler;
title: string;
}
@@ -22,6 +23,7 @@
confirmText,
isOpen = $bindable(),
onbeforeclose,
oncancel,
onConfirmClick,
title,
}: Props = $props();
@@ -31,6 +33,11 @@
isOpen = false;
};
const cancelAction = () => {
oncancel?.();
closeModal();
};
const confirmAction = async () => {
if ((await onConfirmClick()) !== false) {
closeModal();
@@ -38,13 +45,13 @@
};
</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">
<p class="text-xl font-bold">{title}</p>
{@render children()}
</div>
<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>
</div>
</Modal>

View File

@@ -46,17 +46,32 @@ export const refreshSession = async (
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 {
const res = await db
.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");
}
await db.transaction().execute(async (trx) => {
if (force) {
await trx
.deleteFrom("session")
.where("id", "!=", sessionId)
.where("user_id", "=", userId)
.where("client_id", "=", clientId)
.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) {
if (e instanceof pg.DatabaseError && e.code === "23505") {
throw new IntegrityError("Session already exists");

View File

@@ -4,28 +4,29 @@ export const passwordChangeRequest = z.object({
oldPassword: 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({
email: z.string().email(),
password: z.string().trim().nonempty(),
});
export type LoginRequest = z.infer<typeof loginRequest>;
export type LoginRequest = z.input<typeof loginRequest>;
export const sessionUpgradeRequest = z.object({
encPubKey: 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({
id: z.number().int().positive(),
challenge: z.string().base64().nonempty(),
});
export type SessionUpgradeResponse = z.infer<typeof sessionUpgradeResponse>;
export type SessionUpgradeResponse = z.output<typeof sessionUpgradeResponse>;
export const sessionUpgradeVerifyRequest = z.object({
id: z.number().int().positive(),
answerSig: z.string().base64().nonempty(),
force: z.boolean().default(false),
});
export type SessionUpgradeVerifyRequest = z.infer<typeof sessionUpgradeVerifyRequest>;
export type SessionUpgradeVerifyRequest = z.input<typeof sessionUpgradeVerifyRequest>;

View File

@@ -15,12 +15,12 @@ export const categoryInfoResponse = z.object({
.optional(),
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({
file: z.number().int().positive(),
});
export type CategoryFileAddRequest = z.infer<typeof categoryFileAddRequest>;
export type CategoryFileAddRequest = z.input<typeof categoryFileAddRequest>;
export const categoryFileListResponse = z.object({
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({
file: z.number().int().positive(),
});
export type CategoryFileRemoveRequest = z.infer<typeof categoryFileRemoveRequest>;
export type CategoryFileRemoveRequest = z.input<typeof categoryFileRemoveRequest>;
export const categoryRenameRequest = z.object({
dekVersion: z.string().datetime(),
name: 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({
parent: categoryIdSchema,
@@ -52,4 +52,4 @@ export const categoryCreateRequest = z.object({
name: z.string().base64().nonempty(),
nameIv: z.string().base64().nonempty(),
});
export type CategoryCreateRequest = z.infer<typeof categoryCreateRequest>;
export type CategoryCreateRequest = z.input<typeof categoryCreateRequest>;

View File

@@ -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({
encPubKey: 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({
id: z.number().int().positive(),
challenge: z.string().base64().nonempty(),
});
export type ClientRegisterResponse = z.infer<typeof clientRegisterResponse>;
export type ClientRegisterResponse = z.output<typeof clientRegisterResponse>;
export const clientRegisterVerifyRequest = z.object({
id: z.number().int().positive(),
answerSig: z.string().base64().nonempty(),
});
export type ClientRegisterVerifyRequest = z.infer<typeof clientRegisterVerifyRequest>;
export type ClientRegisterVerifyRequest = z.input<typeof clientRegisterVerifyRequest>;
export const clientStatusResponse = z.object({
id: z.number().int().positive(),
state: z.enum(["pending", "active"]),
isInitialMekNeeded: z.boolean(),
});
export type ClientStatusResponse = z.infer<typeof clientStatusResponse>;
export type ClientStatusResponse = z.output<typeof clientStatusResponse>;

View File

@@ -16,19 +16,19 @@ export const directoryInfoResponse = z.object({
subDirectories: 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({
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({
dekVersion: z.string().datetime(),
name: 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({
parent: directoryIdSchema,
@@ -38,4 +38,4 @@ export const directoryCreateRequest = z.object({
name: z.string().base64().nonempty(),
nameIv: z.string().base64().nonempty(),
});
export type DirectoryCreateRequest = z.infer<typeof directoryCreateRequest>;
export type DirectoryCreateRequest = z.input<typeof directoryCreateRequest>;

View File

@@ -21,42 +21,42 @@ export const fileInfoResponse = z.object({
lastModifiedAtIv: z.string().base64().nonempty(),
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({
dekVersion: z.string().datetime(),
name: 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({
updatedAt: z.string().datetime(),
contentIv: z.string().base64().nonempty(),
});
export type FileThumbnailInfoResponse = z.infer<typeof fileThumbnailInfoResponse>;
export type FileThumbnailInfoResponse = z.output<typeof fileThumbnailInfoResponse>;
export const fileThumbnailUploadRequest = z.object({
dekVersion: z.string().datetime(),
contentIv: z.string().base64().nonempty(),
});
export type FileThumbnailUploadRequest = z.infer<typeof fileThumbnailUploadRequest>;
export type FileThumbnailUploadRequest = z.input<typeof fileThumbnailUploadRequest>;
export const duplicateFileScanRequest = z.object({
hskVersion: z.number().int().positive(),
contentHmac: z.string().base64().nonempty(),
});
export type DuplicateFileScanRequest = z.infer<typeof duplicateFileScanRequest>;
export type DuplicateFileScanRequest = z.input<typeof duplicateFileScanRequest>;
export const duplicateFileScanResponse = z.object({
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({
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({
parent: directoryIdSchema,
@@ -78,9 +78,9 @@ export const fileUploadRequest = z.object({
lastModifiedAt: 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({
file: z.number().int().positive(),
});
export type FileUploadResponse = z.infer<typeof fileUploadResponse>;
export type FileUploadResponse = z.output<typeof fileUploadResponse>;

View File

@@ -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({
mekVersion: z.number().int().positive(),
hsk: z.string().base64().nonempty(),
});
export type InitialHmacSecretRegisterRequest = z.infer<typeof initialHmacSecretRegisterRequest>;
export type InitialHmacSecretRegisterRequest = z.input<typeof initialHmacSecretRegisterRequest>;

View File

@@ -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({
mek: z.string().base64().nonempty(),
mekSig: z.string().base64().nonempty(),
});
export type InitialMasterKeyRegisterRequest = z.infer<typeof initialMasterKeyRegisterRequest>;
export type InitialMasterKeyRegisterRequest = z.input<typeof initialMasterKeyRegisterRequest>;

View File

@@ -4,9 +4,9 @@ export const userInfoResponse = z.object({
email: z.string().email(),
nickname: z.string().nonempty(),
});
export type UserInfoResponse = z.infer<typeof userInfoResponse>;
export type UserInfoResponse = z.output<typeof userInfoResponse>;
export const nicknameChangeRequest = z.object({
newNickname: z.string().trim().min(2).max(8),
});
export type NicknameChangeRequest = z.infer<typeof nicknameChangeRequest>;
export type NicknameChangeRequest = z.input<typeof nicknameChangeRequest>;

View File

@@ -87,9 +87,11 @@ export const createSessionUpgradeChallenge = async (
export const verifySessionUpgradeChallenge = async (
sessionId: string,
userId: number,
ip: string,
challengeId: number,
answerSig: string,
force: boolean,
) => {
const challenge = await consumeSessionUpgradeChallenge(challengeId, sessionId, ip);
if (!challenge) {
@@ -106,13 +108,13 @@ export const verifySessionUpgradeChallenge = async (
}
try {
await upgradeSession(sessionId, client.id);
await upgradeSession(userId, sessionId, client.id, force);
} catch (e) {
if (e instanceof IntegrityError) {
if (e.message === "Session not found") {
error(500, "Invalid challenge answer");
} else if (e.message === "Session already exists") {
error(403, "Already logged in");
} else if (!force && e.message === "Session already exists") {
error(409, "Already logged in");
}
}
throw e;

View File

@@ -11,12 +11,14 @@ export const requestSessionUpgrade = async (
decryptKey: CryptoKey,
verifyKeyBase64: string,
signKey: CryptoKey,
force = false,
) => {
let res = await callPostApi<SessionUpgradeRequest>("/api/auth/upgradeSession", {
encPubKey: encryptKeyBase64,
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 answer = await decryptChallenge(challenge, decryptKey);
@@ -25,6 +27,13 @@ export const requestSessionUpgrade = async (
res = await callPostApi<SessionUpgradeVerifyRequest>("/api/auth/upgradeSession/verify", {
id,
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;
};