mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
perf(theme): 优化 站点加密交互
This commit is contained in:
parent
31a5c01d2d
commit
d49e70f48e
@ -58,4 +58,5 @@ export default defineThemeConfig({
|
||||
'/article/enx7c9s/': '123456',
|
||||
},
|
||||
},
|
||||
autoFrontmatter: { exclude: ['**/*.snippet.*'] },
|
||||
})
|
||||
|
||||
@ -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 },
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useData } from '../composables/data.js'
|
||||
import { useEncryptCompare } from '../composables/encrypt.js'
|
||||
|
||||
const props = defineProps<{
|
||||
compare: (password: string) => boolean
|
||||
global?: boolean
|
||||
info?: string
|
||||
}>()
|
||||
|
||||
const { theme } = useData()
|
||||
const { compareGlobal, comparePage } = useEncryptCompare()
|
||||
|
||||
const password = ref('')
|
||||
const errorCode = ref(0) // 0: no error, 1: wrong password
|
||||
const unlocking = ref(false)
|
||||
|
||||
function onSubmit() {
|
||||
const result = props.compare(password.value)
|
||||
async function onSubmit() {
|
||||
if (unlocking.value)
|
||||
return
|
||||
|
||||
const compare = props.global ? compareGlobal : comparePage
|
||||
unlocking.value = true
|
||||
const result = await compare(password.value)
|
||||
unlocking.value = false
|
||||
if (!result) {
|
||||
errorCode.value = 1
|
||||
}
|
||||
@ -40,8 +48,9 @@ function onSubmit() {
|
||||
@input="password && (errorCode = 0)"
|
||||
>
|
||||
</p>
|
||||
<button class="encrypt-button" @click="onSubmit">
|
||||
{{ theme.encryptButtonText ?? 'Confirm' }}
|
||||
<button class="encrypt-button" :class="{ unlocking }" @click="onSubmit">
|
||||
<span v-if="!unlocking">{{ theme.encryptButtonText ?? 'Confirm' }}</span>
|
||||
<span v-else class="vpi-loading" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@ -104,4 +113,14 @@ function onSubmit() {
|
||||
.encrypt-button:hover {
|
||||
background-color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.encrypt-button.unlocking {
|
||||
color: var(--vp-c-brand-1);
|
||||
background-color: var(--vp-c-gray-1);
|
||||
}
|
||||
|
||||
.vpi-loading {
|
||||
display: inline-block;
|
||||
transform: scale(5);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,10 +3,8 @@ import { computed } from 'vue'
|
||||
import VPFooter from '@theme/VPFooter.vue'
|
||||
import VPEncryptForm from '@theme/VPEncryptForm.vue'
|
||||
import { useData } from '../composables/data.js'
|
||||
import { useGlobalEncrypt } from '../composables/encrypt.js'
|
||||
|
||||
const { theme, site } = useData()
|
||||
const { compareGlobal } = useGlobalEncrypt()
|
||||
|
||||
const profile = computed(() => theme.value.profile)
|
||||
const title = computed(() => profile.value?.name || site.value.title)
|
||||
@ -23,7 +21,7 @@ const title = computed(() => profile.value?.name || site.value.title)
|
||||
{{ title }}
|
||||
</h3>
|
||||
</div>
|
||||
<VPEncryptForm :compare="compareGlobal" :info="theme.encryptGlobalText" />
|
||||
<VPEncryptForm global :info="theme.encryptGlobalText" />
|
||||
</div>
|
||||
</div>
|
||||
<VPFooter />
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import VPEncryptForm from '@theme/VPEncryptForm.vue'
|
||||
import { usePageEncrypt } from '../composables/encrypt.js'
|
||||
import { useData } from '../composables/data.js'
|
||||
|
||||
const { theme } = useData()
|
||||
const { comparePage } = usePageEncrypt()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -12,7 +10,7 @@ const { comparePage } = usePageEncrypt()
|
||||
<div class="logo">
|
||||
<span class="vpi-lock icon-lock-head" />
|
||||
</div>
|
||||
<VPEncryptForm :compare="comparePage" :info="theme.encryptPageText" />
|
||||
<VPEncryptForm :info="theme.encryptPageText" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -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<string, string>, // 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<EncryptData>
|
||||
|
||||
@ -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<boolean>
|
||||
isGlobalDecrypted: Ref<boolean>
|
||||
isPageDecrypted: Ref<boolean>
|
||||
hashList: Ref<EncryptDataRule[]>
|
||||
}
|
||||
|
||||
export const EncryptSymbol: InjectionKey<Encrypt> = 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<string, boolean>()
|
||||
function compare(content: string, hash: string, separator = ':') {
|
||||
const compareCache = new Map<string, boolean>()
|
||||
async function compareDecrypt(content: string, hash: string, separator = ':'): Promise<boolean> {
|
||||
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<boolean>
|
||||
compareGlobal: (password: string) => boolean
|
||||
} {
|
||||
const matchCache = new Map<string, RegExp>()
|
||||
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 }
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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)
|
||||
</template>
|
||||
</VPNav>
|
||||
|
||||
<VPLocalNav :open="isSidebarOpen" :show-outline="isPageDecrypted" @open-menu="openSidebar" />
|
||||
<VPLocalNav
|
||||
:open="isSidebarOpen"
|
||||
:show-outline="isPageDecrypted"
|
||||
@open-menu="openSidebar"
|
||||
/>
|
||||
|
||||
<VPSidebar :open="isSidebarOpen">
|
||||
<template #sidebar-nav-before>
|
||||
|
||||
@ -89,6 +89,10 @@
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E %3Cpath fill='currentColor' d='M18 8h-1V7c0-2.757-2.243-5-5-5S7 4.243 7 7v1H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2M9 7c0-1.654 1.346-3 3-3s3 1.346 3 3v1H9zm4 8.723V18h-2v-2.277c-.595-.346-1-.984-1-1.723a2 2 0 1 1 4 0c0 .738-.405 1.376-1 1.723' /%3E %3C/svg%3E");
|
||||
}
|
||||
|
||||
.vpi-loading {
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Ccircle cx='18' cy='12' r='0' fill='%23000'%3E%3Canimate attributeName='r' begin='.67' calcMode='spline' dur='1.5s' keySplines='0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8' repeatCount='indefinite' values='0;2;0;0'/%3E%3C/circle%3E%3Ccircle cx='12' cy='12' r='0' fill='%23000'%3E%3Canimate attributeName='r' begin='.33' calcMode='spline' dur='1.5s' keySplines='0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8' repeatCount='indefinite' values='0;2;0;0'/%3E%3C/circle%3E%3Ccircle cx='6' cy='12' r='0' fill='%23000'%3E%3Canimate attributeName='r' begin='0' calcMode='spline' dur='1.5s' keySplines='0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8' repeatCount='indefinite' values='0;2;0;0'/%3E%3C/circle%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.vpi-print {
|
||||
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M16 8V5H8v3H6V3h12v5zM4 10h16zm14 2.5q.425 0 .713-.288T19 11.5q0-.425-.288-.712T18 10.5q-.425 0-.712.288T17 11.5q0 .425.288.713T18 12.5M16 19v-4H8v4zm2 2H6v-4H2v-6q0-1.275.875-2.137T5 8h14q1.275 0 2.138.863T22 11v6h-4zm2-6v-4q0-.425-.288-.712T19 10H5q-.425 0-.712.288T4 11v4h2v-2h12v2z' /%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user