2025-11-09 14:59:10 +08:00

219 lines
5.8 KiB
TypeScript

import type { InjectionKey, Ref } from 'vue'
import type { EncryptDataRule } from './encrypt-data.js'
import { hasOwn, useSessionStorage } from '@vueuse/core'
import { compare, genSaltSync } from 'bcrypt-ts/browser'
import { computed, inject, provide } from 'vue'
import { useRoute } from 'vuepress/client'
import { removeLeadingSlash } from 'vuepress/shared'
import { useData } from './data.js'
import { 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', () => {
if (__VUEPRESS_SSR__) {
return { s: ['', ''] as const, g: '', p: {} as Record<string, string> }
}
return {
s: [genSaltSync(10), genSaltSync(10)] as const,
g: '' as string,
p: {} as Record<string, string>,
}
})
function mergeHash(hash: string) {
const [left, right] = storage.value.s
return left + hash + right
}
function splitHash(hash: string) {
const [left, right] = storage.value.s
if (!hash.startsWith(left) || !hash.endsWith(right))
return ''
return hash.slice(left.length, hash.length - right.length)
}
const compareCache = new Map<string, boolean>()
async function compareDecrypt(content: string, hash: string, separator = ':'): Promise<boolean> {
const key = [content, hash].join(separator)
if (compareCache.has(key))
return compareCache.get(key)!
try {
const result = await compare(content, hash)
compareCache.set(key, result)
return result
}
catch {
compareCache.set(key, false)
return false
}
}
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) || regex.test(relativePath)
}
if (match.endsWith('.md'))
return relativePath && relativePath.endsWith(match)
return pagePath.startsWith(match) || relativePath.startsWith(removeLeadingSlash(match))
}
export function setupEncrypt(): void {
const { page } = useData()
const route = useRoute()
const encrypt = useEncryptData()
const hasPageEncrypt = computed(() => {
const pagePath = route.path
const filePathRelative = page.value.filePathRelative
if (page.value._e)
return true
return encrypt.value.ruleList.length
? encrypt.value.matches.some(match => toMatch(match, pagePath, filePathRelative))
: false
})
const isGlobalDecrypted = computed(() => {
if (!encrypt.value.global)
return true
const hash = splitHash(storage.value.g)
return !!hash && encrypt.value.admins.includes(hash)
})
const hashList = computed(() => {
const pagePath = route.path
const filePathRelative = page.value.filePathRelative
const passwords = typeof page.value._e === 'string' ? page.value._e.split(':') : []
const pageRule: EncryptDataRule | undefined = passwords.length
? { key: pagePath.replace(/\//g, '').replace(/\.html$/, ''), match: pagePath, rules: passwords }
: undefined
const rules = encrypt.value.ruleList.length
? encrypt.value.ruleList
.filter(item => toMatch(item.match, pagePath, filePathRelative))
: []
return [pageRule, ...rules].filter(Boolean) as EncryptDataRule[]
})
const isPageDecrypted = computed(() => {
if (!hasPageEncrypt.value)
return true
const hash = splitHash(storage.value.g || '')
if (hash && encrypt.value.admins.includes(hash))
return true
for (const { key, rules } of hashList.value) {
if (hasOwn(storage.value.p, key)) {
const hash = splitHash(storage.value.p[key])
if (hash && rules.includes(hash))
return true
}
}
return false
})
provide(EncryptSymbol, {
hasPageEncrypt,
isGlobalDecrypted,
isPageDecrypted,
hashList,
})
}
export function useEncrypt(): Encrypt {
const result = inject(EncryptSymbol)
if (!result)
throw new Error('useEncrypt() is called without setup')
return result
}
export function useEncryptCompare(): {
compareGlobal: (password: string) => Promise<boolean>
comparePage: (password: string) => Promise<boolean>
} {
const encrypt = useEncryptData()
const { page } = useData()
const route = useRoute()
const { hashList } = useEncrypt()
async function compareGlobal(password: string): Promise<boolean> {
if (!password)
return false
for (const admin of encrypt.value.admins) {
if (await compareDecrypt(password, admin, encrypt.value.separator)) {
storage.value.g = mergeHash(admin)
return true
}
}
return false
}
async function comparePage(password: string): Promise<boolean> {
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),
}
break
}
}
if (decrypted)
break
}
}
if (!decrypted) {
decrypted = await compareGlobal(password)
}
return decrypted
}
return { compareGlobal, comparePage }
}