mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(theme): migrate bcrypt-ts to hash-wasm (#774)
This commit is contained in:
parent
32f4a8be5a
commit
4b1cecf2bd
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@ -209,9 +209,6 @@ catalogs:
|
||||
'@vueuse/integrations':
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0
|
||||
bcrypt-ts:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
cac:
|
||||
specifier: ^6.7.14
|
||||
version: 6.7.14
|
||||
@ -236,6 +233,9 @@ catalogs:
|
||||
handlebars:
|
||||
specifier: ^4.7.8
|
||||
version: 4.7.8
|
||||
hash-wasm:
|
||||
specifier: ^4.12.0
|
||||
version: 4.12.0
|
||||
image-size:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
@ -863,9 +863,6 @@ importers:
|
||||
'@vueuse/core':
|
||||
specifier: catalog:prod
|
||||
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
||||
bcrypt-ts:
|
||||
specifier: catalog:prod
|
||||
version: 7.1.0
|
||||
chokidar:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
@ -878,6 +875,9 @@ importers:
|
||||
gray-matter:
|
||||
specifier: catalog:prod
|
||||
version: 4.0.3
|
||||
hash-wasm:
|
||||
specifier: catalog:prod
|
||||
version: 4.12.0
|
||||
js-yaml:
|
||||
specifier: catalog:prod
|
||||
version: 4.1.1
|
||||
@ -3299,10 +3299,6 @@ packages:
|
||||
bcp-47@2.1.0:
|
||||
resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==}
|
||||
|
||||
bcrypt-ts@7.1.0:
|
||||
resolution: {integrity: sha512-t/Dqr9YzYmn/+oPQBgotBPUuezpZD5CPBwapM5Ep1p3zsLmEycMdXOfZpWbztSBWJ41DlB7EluJBUDsAGSiUeQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
birpc@2.6.1:
|
||||
resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==}
|
||||
|
||||
@ -4821,6 +4817,9 @@ packages:
|
||||
hash-sum@2.0.0:
|
||||
resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==}
|
||||
|
||||
hash-wasm@4.12.0:
|
||||
resolution: {integrity: sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==}
|
||||
|
||||
hashery@1.2.0:
|
||||
resolution: {integrity: sha512-43XJKpwle72Ik5Zpam7MuzRWyNdwwdf6XHlh8wCj2PggvWf+v/Dm5B0dxGZOmddidgeO6Ofu9As/o231Ti/9PA==}
|
||||
engines: {node: '>=20'}
|
||||
@ -10207,8 +10206,6 @@ snapshots:
|
||||
is-alphanumerical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
|
||||
bcrypt-ts@7.1.0: {}
|
||||
|
||||
birpc@2.6.1: {}
|
||||
|
||||
birpc@2.8.0: {}
|
||||
@ -11983,6 +11980,8 @@ snapshots:
|
||||
|
||||
hash-sum@2.0.0: {}
|
||||
|
||||
hash-wasm@4.12.0: {}
|
||||
|
||||
hashery@1.2.0:
|
||||
dependencies:
|
||||
hookified: 1.13.0
|
||||
|
||||
@ -88,7 +88,6 @@ catalogs:
|
||||
'@pengzhanbo/utils': ^2.1.2
|
||||
'@vueuse/core': ^14.1.0
|
||||
'@vueuse/integrations': ^14.1.0
|
||||
bcrypt-ts: ^7.1.0
|
||||
cac: ^6.7.14
|
||||
chart.js: ^4.5.1
|
||||
chokidar: 5.0.0
|
||||
@ -99,6 +98,7 @@ catalogs:
|
||||
focus-trap: ^7.6.6
|
||||
gray-matter: ^4.0.3
|
||||
handlebars: ^4.7.8
|
||||
hash-wasm: ^4.12.0
|
||||
image-size: ^2.0.2
|
||||
js-yaml: ^4.1.1
|
||||
katex: ^0.16.25
|
||||
|
||||
@ -129,11 +129,11 @@
|
||||
"@vuepress/plugin-sitemap": "catalog:vuepress",
|
||||
"@vuepress/plugin-watermark": "catalog:vuepress",
|
||||
"@vueuse/core": "catalog:prod",
|
||||
"bcrypt-ts": "catalog:prod",
|
||||
"chokidar": "catalog:prod",
|
||||
"dayjs": "catalog:prod",
|
||||
"esbuild": "catalog:prod",
|
||||
"gray-matter": "catalog:prod",
|
||||
"hash-wasm": "catalog:prod",
|
||||
"js-yaml": "catalog:prod",
|
||||
"katex": "catalog:prod",
|
||||
"local-pkg": "catalog:prod",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { computedAsync, useSessionStorage } from '@vueuse/core'
|
||||
import { bcryptVerify, md5 } from 'hash-wasm'
|
||||
import { computed, inject, provide } from 'vue'
|
||||
import { useRoute } from 'vuepress/client'
|
||||
import { removeLeadingSlash } from 'vuepress/shared'
|
||||
@ -21,28 +21,14 @@ export const EncryptSymbol: InjectionKey<Encrypt> = Symbol(
|
||||
|
||||
const storage = useSessionStorage('2a0a3d6afb2fdf1f', () => {
|
||||
if (__VUEPRESS_SSR__) {
|
||||
return { s: ['', ''] as const, g: '', p: {} as Record<string, string> }
|
||||
return { g: '', p: [] as string[] }
|
||||
}
|
||||
return {
|
||||
s: [genSaltSync(10), genSaltSync(10)] as const,
|
||||
g: '' as string,
|
||||
p: {} as Record<string, string>,
|
||||
g: '',
|
||||
p: [] as 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)
|
||||
@ -50,7 +36,7 @@ async function compareDecrypt(content: string, hash: string, separator = ':'): P
|
||||
return compareCache.get(key)!
|
||||
|
||||
try {
|
||||
const result = await compare(content, hash)
|
||||
const result = await bcryptVerify({ password: content, hash })
|
||||
compareCache.set(key, result)
|
||||
return result
|
||||
}
|
||||
@ -98,14 +84,17 @@ export function setupEncrypt(): void {
|
||||
: false
|
||||
})
|
||||
|
||||
const isGlobalDecrypted = computed(() => {
|
||||
const isGlobalDecrypted = computedAsync(async () => {
|
||||
const hash = storage.value.g
|
||||
if (!encrypt.value.global)
|
||||
return true
|
||||
|
||||
const hash = splitHash(storage.value.g)
|
||||
|
||||
return !!hash && encrypt.value.admins.includes(hash)
|
||||
})
|
||||
for (const admin of encrypt.value.admins) {
|
||||
if (hash && hash === await md5(admin))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, !encrypt.value.global)
|
||||
|
||||
const hashList = computed(() => {
|
||||
const pagePath = route.path
|
||||
@ -122,24 +111,27 @@ export function setupEncrypt(): void {
|
||||
return [pageRule, ...rules].filter(Boolean) as EncryptDataRule[]
|
||||
})
|
||||
|
||||
const isPageDecrypted = computed(() => {
|
||||
const isPageDecrypted = computedAsync(async () => {
|
||||
if (!hasPageEncrypt.value)
|
||||
return true
|
||||
|
||||
const hash = splitHash(storage.value.g || '')
|
||||
if (hash && encrypt.value.admins.includes(hash))
|
||||
return true
|
||||
const hash = storage.value.g
|
||||
|
||||
for (const admin of encrypt.value.admins) {
|
||||
if (hash && hash === await md5(admin))
|
||||
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))
|
||||
const hash = storage.value.p[key]
|
||||
for (const rule of rules) {
|
||||
if (hash && hash === await md5(rule))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}, !hasPageEncrypt.value)
|
||||
|
||||
provide(EncryptSymbol, {
|
||||
hasPageEncrypt,
|
||||
@ -173,7 +165,7 @@ export function useEncryptCompare(): {
|
||||
|
||||
for (const admin of encrypt.value.admins) {
|
||||
if (await compareDecrypt(password, admin, encrypt.value.separator)) {
|
||||
storage.value.g = mergeHash(admin)
|
||||
storage.value.g = await md5(admin)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -195,10 +187,7 @@ export function useEncryptCompare(): {
|
||||
for (const rule of rules) {
|
||||
if (await compareDecrypt(password, rule, encrypt.value.separator)) {
|
||||
decrypted = true
|
||||
storage.value.p = {
|
||||
...storage.value.p,
|
||||
[key]: mergeHash(rule),
|
||||
}
|
||||
storage.value.p[key] = await md5(rule)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export function extendsBundlerOptions(bundlerOptions: any, app: App): void {
|
||||
addViteOptimizeDepsInclude(
|
||||
bundlerOptions,
|
||||
app,
|
||||
['@vueuse/core', 'bcrypt-ts/browser', '@vuepress/helper/client', '@iconify/vue', '@iconify/vue/offline', '@vuepress/plugin-git/client', '@vuepress/plugin-markdown-chart/client'],
|
||||
['@vueuse/core', 'hash-wasm', '@vuepress/helper/client', '@iconify/vue', '@iconify/vue/offline', '@vuepress/plugin-git/client', '@vuepress/plugin-markdown-chart/client'],
|
||||
)
|
||||
addViteOptimizeDepsExclude(bundlerOptions, app, '@theme')
|
||||
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import type { Page } from 'vuepress/core'
|
||||
import type { ThemePageData } from '../../shared/index.js'
|
||||
import { toArray } from '@pengzhanbo/utils'
|
||||
import pMap from 'p-map'
|
||||
import { genEncrypt } from '../utils/index.js'
|
||||
|
||||
export function encryptPage(
|
||||
export async function encryptPage(
|
||||
page: Page<ThemePageData>,
|
||||
): void {
|
||||
const password = toArray(page.frontmatter.password)
|
||||
): Promise<void> {
|
||||
const password = toArray(page.frontmatter.password) as string[]
|
||||
if (password.length) {
|
||||
page.data._e = password.map(pwd => genEncrypt(pwd as string)).join(':')
|
||||
page.data._e = (await pMap(password, item => genEncrypt(item))).join(':')
|
||||
}
|
||||
delete page.frontmatter.password
|
||||
}
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
import type { Page } from 'vuepress/core'
|
||||
import type { ThemePageData } from '../../shared/index.js'
|
||||
import { getThemeConfig } from '../loadConfig/index.js'
|
||||
import { autoCategory } from './autoCategory.js'
|
||||
import { encryptPage } from './encryptPage.js'
|
||||
import { enableBulletin } from './pageBulletin.js'
|
||||
|
||||
export function extendsPageData(
|
||||
export async function extendsPageData(
|
||||
page: Page<ThemePageData>,
|
||||
): void {
|
||||
const options = getThemeConfig()
|
||||
): Promise<void> {
|
||||
cleanPageData(page)
|
||||
encryptPage(page)
|
||||
autoCategory(page)
|
||||
enableBulletin(page, options)
|
||||
enableBulletin(page)
|
||||
await encryptPage(page)
|
||||
}
|
||||
|
||||
function cleanPageData(page: Page<ThemePageData>) {
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import type { Page } from 'vuepress/core'
|
||||
import type { ThemeOptions, ThemePageData } from '../../shared/index.js'
|
||||
import type { ThemePageData } from '../../shared/index.js'
|
||||
import { isFunction, isPlainObject } from '@vuepress/helper'
|
||||
import { getThemeConfig } from '../loadConfig/index.js'
|
||||
|
||||
export function enableBulletin(
|
||||
page: Page<ThemePageData>,
|
||||
options: ThemeOptions,
|
||||
): void {
|
||||
const options = getThemeConfig()
|
||||
if (isPlainObject(options.bulletin)) {
|
||||
const enablePage = options.bulletin.enablePage
|
||||
page.data.bulletin = (isFunction(enablePage) ? enablePage(page) : enablePage) ?? true
|
||||
|
||||
@ -4,6 +4,7 @@ import type { EncryptOptions, ThemePageData } from '../../shared/index.js'
|
||||
import type { FsCache } from '../utils/index.js'
|
||||
import { isEmptyObject, isNumber, isString, toArray } from '@pengzhanbo/utils'
|
||||
import { encodeData, removeLeadingSlash } from '@vuepress/helper'
|
||||
import pMap from 'p-map'
|
||||
import { getThemeConfig } from '../loadConfig/index.js'
|
||||
import { createFsCache, genEncrypt, hash, perf, resolveContent, writeTemp } from '../utils/index.js'
|
||||
|
||||
@ -34,7 +35,7 @@ export async function prepareEncrypt(app: App): Promise<void> {
|
||||
|
||||
if (!contentHash || contentHash !== currentHash || !resolvedEncrypt) {
|
||||
contentHash = currentHash
|
||||
resolvedEncrypt = resolveEncrypt(encrypt)
|
||||
resolvedEncrypt = await resolveEncrypt(encrypt)
|
||||
}
|
||||
await writeTemp(app, 'internal/encrypt.js', resolveContent(app, {
|
||||
name: 'encrypt',
|
||||
@ -46,12 +47,12 @@ export async function prepareEncrypt(app: App): Promise<void> {
|
||||
perf.log('prepare:encrypt')
|
||||
}
|
||||
|
||||
function resolveEncrypt(encrypt?: EncryptOptions): EncryptConfig {
|
||||
async function resolveEncrypt(encrypt?: EncryptOptions): Promise<EncryptConfig> {
|
||||
const admin = encrypt?.admin
|
||||
? toArray(encrypt.admin)
|
||||
.filter(isStringLike)
|
||||
.map(item => genEncrypt(item))
|
||||
.join(separator)
|
||||
? (await pMap(
|
||||
toArray(encrypt.admin).filter(isStringLike),
|
||||
item => genEncrypt(item),
|
||||
)).join(separator)
|
||||
: ''
|
||||
|
||||
const encryptRules = Object.keys(encrypt?.rules ?? {}).reduce((acc, key) => {
|
||||
@ -63,14 +64,13 @@ function resolveEncrypt(encrypt?: EncryptOptions): EncryptConfig {
|
||||
const keys = Object.keys(encryptRules)
|
||||
|
||||
if (!isEmptyObject(encryptRules)) {
|
||||
Object.keys(encryptRules).forEach((key) => {
|
||||
for (const key of keys) {
|
||||
const index = keys.indexOf(key)
|
||||
|
||||
rules[String(index)] = toArray(encryptRules[key])
|
||||
.filter(isStringLike)
|
||||
.map(item => genEncrypt(item))
|
||||
.join(separator)
|
||||
})
|
||||
rules[String(index)] = (await pMap(
|
||||
toArray(encryptRules[key]).filter(isStringLike),
|
||||
item => genEncrypt(item),
|
||||
)).join(separator)
|
||||
}
|
||||
}
|
||||
|
||||
return [encrypt?.global ?? false, separator, admin, keys, rules]
|
||||
|
||||
@ -72,7 +72,7 @@ export function plumeTheme(options: ThemeOptions = {}): Theme {
|
||||
|
||||
templateBuildRenderer,
|
||||
|
||||
extendsPage: page => extendsPageData(page as Page<ThemePageData>),
|
||||
extendsPage: async page => await extendsPageData(page as Page<ThemePageData>),
|
||||
|
||||
onInitialized: async app => await createPages(app),
|
||||
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
import { random } from '@pengzhanbo/utils'
|
||||
import { genSaltSync, hashSync } from 'bcrypt-ts'
|
||||
import crypto from 'node:crypto'
|
||||
import { bcrypt } from 'hash-wasm'
|
||||
|
||||
export function genEncrypt(pwd: string): string {
|
||||
return hashSync(String(pwd), genSaltSync(random(8, 16)))
|
||||
export async function genEncrypt(password: string): Promise<string> {
|
||||
const salt = new Uint8Array(16)
|
||||
crypto.getRandomValues(salt)
|
||||
return bcrypt({
|
||||
password,
|
||||
salt,
|
||||
costFactor: 11,
|
||||
outputType: 'encoded',
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user