feat: 发布 v0.5.0 加密存储与匿名同步

This commit is contained in:
2026-06-23 01:48:01 +02:00
parent 81a70137a9
commit e3825c5a4e
20 changed files with 1091 additions and 70 deletions
+105
View File
@@ -0,0 +1,105 @@
'use client';
import { AssessmentRecord, EncryptedAssessmentRecord } from '@/lib/assessment-types';
import {
decryptAnonymousRecord,
encryptRecordForAnonymousProfile,
} from '@/lib/record-crypto';
const SESSION_KEY = 'mindscope_anonymous_session';
export interface AnonymousSession {
codeName: string;
password: string;
}
export interface AnonymousProfile {
id: string;
codeName: string;
createdAt: string;
updatedAt: string;
lastSeenAt: string;
}
interface AnonymousEncryptedLoginResult {
profile: AnonymousProfile;
records: EncryptedAssessmentRecord[];
}
export interface AnonymousLoginResult {
profile: AnonymousProfile;
records: AssessmentRecord[];
}
async function parseResponse<T>(response: Response): Promise<T> {
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(typeof data.error === 'string' ? data.error : '请求失败');
}
return data as T;
}
export function getAnonymousSession(): AnonymousSession | null {
try {
const raw = sessionStorage.getItem(SESSION_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as Partial<AnonymousSession>;
return parsed.codeName && parsed.password
? { codeName: parsed.codeName, password: parsed.password }
: null;
} catch {
return null;
}
}
export function saveAnonymousSession(session: AnonymousSession) {
sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));
}
export function clearAnonymousSession() {
sessionStorage.removeItem(SESSION_KEY);
}
async function decryptLoginResult(
encryptedResult: AnonymousEncryptedLoginResult,
session: AnonymousSession,
): Promise<AnonymousLoginResult> {
const records = await Promise.all(
encryptedResult.records.map((record) => decryptAnonymousRecord(record, session.codeName, session.password)),
);
return {
profile: encryptedResult.profile,
records: records.sort((a, b) => b.completedAt.localeCompare(a.completedAt)),
};
}
export async function loginAnonymousProfile(session: AnonymousSession) {
const encryptedResult = await fetch('/api/anonymous/profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(session),
}).then((response) => parseResponse<AnonymousEncryptedLoginResult>(response));
saveAnonymousSession(session);
return decryptLoginResult(encryptedResult, session);
}
export async function syncAnonymousRecord(record: AssessmentRecord, session = getAnonymousSession()) {
if (!session) return null;
const encryptedRecord = await encryptRecordForAnonymousProfile(record, session.codeName, session.password);
const result = await fetch('/api/anonymous/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...session, encryptedRecord }),
}).then((response) => parseResponse<{ record: EncryptedAssessmentRecord }>(response));
return decryptAnonymousRecord(result.record, session.codeName, session.password);
}
export async function deleteAnonymousProfile(session: AnonymousSession) {
const result = await fetch('/api/anonymous/profile', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(session),
}).then((response) => parseResponse<{ ok: boolean }>(response));
clearAnonymousSession();
return result;
}