diff --git a/docs/.vuepress/plume.config.ts b/docs/.vuepress/plume.config.ts index dc5e0acd..6464a896 100644 --- a/docs/.vuepress/plume.config.ts +++ b/docs/.vuepress/plume.config.ts @@ -58,4 +58,5 @@ export default defineThemeConfig({ '/article/enx7c9s/': '123456', }, }, + autoFrontmatter: { exclude: ['**/*.snippet.*'] }, }) diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 0a5ef85d..888bd636 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -5,7 +5,6 @@ import type { Theme } from 'vuepress' export const theme: Theme = plumeTheme({ hostname: process.env.SITE_HOST || 'https://plume.pengzhanbo.cn', plugins: { - frontmatter: { exclude: ['**/*.snippet.*'] }, shiki: { twoslash: true }, diff --git a/theme/src/client/components/VPDoc.vue b/theme/src/client/components/VPDoc.vue index 542c51d8..6ad59ffa 100644 --- a/theme/src/client/components/VPDoc.vue +++ b/theme/src/client/components/VPDoc.vue @@ -6,7 +6,7 @@ import VPDocAside from '@theme/VPDocAside.vue' import VPDocFooter from '@theme/VPDocFooter.vue' import VPEncryptPage from '@theme/VPEncryptPage.vue' import VPDocMeta from '@theme/VPDocMeta.vue' -import { usePageEncrypt } from '../composables/encrypt.js' +import { useEncrypt } from '../composables/encrypt.js' import { useSidebar } from '../composables/sidebar.js' import { useData } from '../composables/data.js' @@ -14,7 +14,7 @@ const { page, theme, frontmatter, isDark } = useData() const route = useRoute() const { hasSidebar, hasAside, leftAside } = useSidebar() -const { isPageDecrypted } = usePageEncrypt() +const { isPageDecrypted } = useEncrypt() const hasComments = computed(() => { return page.value.frontmatter.comments !== false diff --git a/theme/src/client/components/VPEncryptForm.vue b/theme/src/client/components/VPEncryptForm.vue index a246557b..8a305973 100644 --- a/theme/src/client/components/VPEncryptForm.vue +++ b/theme/src/client/components/VPEncryptForm.vue @@ -1,20 +1,28 @@ @@ -33,11 +31,15 @@ const { comparePage } = usePageEncrypt() width: 400px; padding: 20px; margin: 40px auto 0; - background-color: var(--vp-c-bg-alt); + border: solid 1px var(--vp-c-divider); border-radius: 8px; - box-shadow: var(--vp-shadow-2); + box-shadow: var(--vp-shadow-1); transition: var(--t-color); - transition-property: box-shadow, background-color; + transition-property: box-shadow, border-color; + } + + .vp-page-encrypt:hover { + box-shadow: var(--vp-shadow-2); } } diff --git a/theme/src/client/composables/encrypt-data.ts b/theme/src/client/composables/encrypt-data.ts index cb6dc272..a8735a4b 100644 --- a/theme/src/client/composables/encrypt-data.ts +++ b/theme/src/client/composables/encrypt-data.ts @@ -1,6 +1,4 @@ -import { - encrypt as rawEncrypt, -} from '@internal/encrypt' +import { encrypt as rawEncrypt } from '@internal/encrypt' import { ref } from 'vue' import type { Ref } from 'vue' @@ -12,16 +10,18 @@ export type EncryptConfig = readonly [ Record, // rules ] +export interface EncryptDataRule { + key: string + match: string + rules: string[] +} + export interface EncryptData { global: boolean separator: string admins: string[] matches: string[] - ruleList: { - key: string - match: string - rules: string[] - }[] + ruleList: EncryptDataRule[] } export type EncryptRef = Ref diff --git a/theme/src/client/composables/encrypt.ts b/theme/src/client/composables/encrypt.ts index a31a7fb7..71acfa17 100644 --- a/theme/src/client/composables/encrypt.ts +++ b/theme/src/client/composables/encrypt.ts @@ -1,9 +1,21 @@ -import { compareSync, genSaltSync } from 'bcrypt-ts/browser' -import { type Ref, computed } from 'vue' +import { compare, genSaltSync } from 'bcrypt-ts/browser' +import type { InjectionKey, Ref } from 'vue' +import { computed, inject, provide } from 'vue' import { hasOwn, useSessionStorage } from '@vueuse/core' import { useRoute } from 'vuepress/client' import { useData } from './data.js' -import { useEncryptData } from './encrypt-data.js' +import { type EncryptDataRule, useEncryptData } from './encrypt-data.js' + +export interface Encrypt { + hasPageEncrypt: Ref + isGlobalDecrypted: Ref + isPageDecrypted: Ref + hashList: Ref +} + +export const EncryptSymbol: InjectionKey = Symbol( + __VUEPRESS_DEV__ ? 'Encrypt' : '', +) const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => ({ s: [genSaltSync(10), genSaltSync(10)] as const, @@ -24,23 +36,58 @@ function splitHash(hash: string) { return hash.slice(left.length, hash.length - right.length) } -const cache = new Map() -function compare(content: string, hash: string, separator = ':') { +const compareCache = new Map() +async function compareDecrypt(content: string, hash: string, separator = ':'): Promise { const key = [content, hash].join(separator) - if (cache.has(key)) - return cache.get(key) + if (compareCache.has(key)) + return compareCache.get(key)! - const result = compareSync(content, hash) - cache.set(key, result) - return result + try { + const result = await compare(content, hash) + compareCache.set(key, result) + return result + } + catch { + compareCache.set(key, false) + return false + } } -export function useGlobalEncrypt(): { - isGlobalDecrypted: Ref - compareGlobal: (password: string) => boolean -} { +const matchCache = new Map() +function createMatchRegex(match: string) { + if (matchCache.has(match)) + return matchCache.get(match)! + + const regex = new RegExp(match) + matchCache.set(match, regex) + return regex +} + +function toMatch(match: string, pagePath: string, filePathRelative: string | null) { + const relativePath = filePathRelative || '' + if (match[0] === '^') { + const regex = createMatchRegex(match) + return regex.test(pagePath) || (relativePath && regex.test(relativePath)) + } + if (match.endsWith('.md')) + return relativePath && relativePath.endsWith(match) + + return pagePath.startsWith(match) || relativePath.startsWith(match) +} + +export function setupEncrypt() { + const { page } = useData() + const route = useRoute() const encrypt = useEncryptData() + const hasPageEncrypt = computed(() => { + const pagePath = route.path + const filePathRelative = page.value.filePathRelative + return encrypt.value.ruleList.length + ? encrypt.value.matches.some(match => toMatch(match, pagePath, filePathRelative)) + : false + }) + const isGlobalDecrypted = computed(() => { if (!encrypt.value.global) return true @@ -50,37 +97,14 @@ export function useGlobalEncrypt(): { return !!hash && encrypt.value.admins.includes(hash) }) - function compareGlobal(password: string) { - if (!password) - return false - - for (const admin of encrypt.value.admins) { - if (compare(password, admin, encrypt.value.separator)) { - storage.value.g = mergeHash(admin) - return true - } - } - - return false - } - - return { - isGlobalDecrypted, - compareGlobal, - } -} - -export function usePageEncrypt() { - const { page } = useData() - const route = useRoute() - const encrypt = useEncryptData() - - const hasPageEncrypt = computed(() => encrypt.value.ruleList.length ? encrypt.value.matches.some(toMatch) : false) - - const hashList = computed(() => encrypt.value.ruleList.length - ? encrypt.value.ruleList - .filter(item => toMatch(item.match)) - : []) + const hashList = computed(() => { + const pagePath = route.path + const filePathRelative = page.value.filePathRelative + return encrypt.value.ruleList.length + ? encrypt.value.ruleList + .filter(item => toMatch(item.match, pagePath, filePathRelative)) + : [] + }) const isPageDecrypted = computed(() => { if (!hasPageEncrypt.value) @@ -101,60 +125,75 @@ export function usePageEncrypt() { return false }) - function toMatch(match: string) { - const relativePath = page.value.filePathRelative || '' - if (match[0] === '^') { - const regex = new RegExp(match) - return regex.test(route.path) || (relativePath && regex.test(relativePath)) - } - if (match.endsWith('.md')) - return relativePath && relativePath.endsWith(match) + provide(EncryptSymbol, { + hasPageEncrypt, + isGlobalDecrypted, + isPageDecrypted, + hashList, + }) +} - return route.path.startsWith(match) || relativePath.startsWith(match) - } +export function useEncrypt(): Encrypt { + const result = inject(EncryptSymbol) - function comparePage(password: string) { + if (!result) + throw new Error('useEncrypt() is called without setup') + + return result +} + +export function useEncryptCompare() { + const encrypt = useEncryptData() + const { page } = useData() + const route = useRoute() + const { hashList } = useEncrypt() + + async function compareGlobal(password: string) { if (!password) return false - let decrypted = false - - // check global for (const admin of encrypt.value.admins) { - if (compare(password, admin, encrypt.value.separator)) { - decrypted = true - storage.value.p = { - ...storage.value.p, - __GLOBAL__: mergeHash(admin), - } - break + if (await compareDecrypt(password, admin, encrypt.value.separator)) { + storage.value.g = mergeHash(admin) + return true } } - // check page - if (!decrypted) { - for (const { match, key, rules } of hashList.value) { - if (toMatch(match)) { - for (const rule of rules) { - if (compare(password, rule, encrypt.value.separator)) { - decrypted = true - storage.value.p = { - ...storage.value.p, - [key]: mergeHash(rule), - } - break + + return false + } + + async function comparePage(password: string) { + if (!password) + return false + + const pagePath = route.path + const filePathRelative = page.value.filePathRelative + + let decrypted = false + + for (const { match, key, rules } of hashList.value) { + if (toMatch(match, pagePath, filePathRelative)) { + for (const rule of rules) { + if (await compareDecrypt(password, rule, encrypt.value.separator)) { + decrypted = true + storage.value.p = { + ...storage.value.p, + [key]: mergeHash(rule), } - } - if (decrypted) break + } } + if (decrypted) + break } } + if (!decrypted) { + decrypted = await compareGlobal(password) + } + return decrypted } - return { - isPageDecrypted, - comparePage, - } + return { compareGlobal, comparePage } } diff --git a/theme/src/client/config.ts b/theme/src/client/config.ts index cef6ae7f..c1e45453 100644 --- a/theme/src/client/config.ts +++ b/theme/src/client/config.ts @@ -2,7 +2,14 @@ import './styles/index.css' import { defineClientConfig } from 'vuepress/client' import type { ClientConfig } from 'vuepress/client' -import { enhanceScrollBehavior, setupDarkMode, setupSidebar, setupThemeData, setupWatermark } from './composables/index.js' +import { + enhanceScrollBehavior, + setupDarkMode, + setupEncrypt, + setupSidebar, + setupThemeData, + setupWatermark, +} from './composables/index.js' import { globalComponents } from './globalComponents.js' import Layout from './layouts/Layout.vue' import NotFound from './layouts/NotFound.vue' @@ -16,6 +23,7 @@ export default defineClientConfig({ }, setup() { setupSidebar() + setupEncrypt() setupWatermark() }, layouts: { Layout, NotFound }, diff --git a/theme/src/client/layouts/Layout.vue b/theme/src/client/layouts/Layout.vue index 1f9ef3ec..3a0e2fe4 100644 --- a/theme/src/client/layouts/Layout.vue +++ b/theme/src/client/layouts/Layout.vue @@ -11,7 +11,7 @@ import VPFooter from '@theme/VPFooter.vue' import VPBackToTop from '@theme/VPBackToTop.vue' import VPEncryptGlobal from '@theme/VPEncryptGlobal.vue' import { useCloseSidebarOnEscape, useSidebar } from '../composables/sidebar.js' -import { useGlobalEncrypt, usePageEncrypt } from '../composables/encrypt.js' +import { useEncrypt } from '../composables/encrypt.js' import { useData } from '../composables/data.js' const { @@ -21,8 +21,7 @@ const { } = useSidebar() const { frontmatter } = useData() -const { isGlobalDecrypted } = useGlobalEncrypt() -const { isPageDecrypted } = usePageEncrypt() +const { isGlobalDecrypted, isPageDecrypted } = useEncrypt() const route = useRoute() watch(() => route.path, closeSidebar) @@ -65,7 +64,11 @@ useCloseSidebarOnEscape(isSidebarOpen, closeSidebar) - +