3.0.0.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. const { v4: uuid } = require('uuid')
  2. const bcrypt = require('bcryptjs-then')
  3. const crypto = require('crypto')
  4. const pem2jwk = require('pem-jwk').pem2jwk
  5. /* global WIKI */
  6. exports.up = async knex => {
  7. WIKI.logger.info('Running 3.0.0 database migration...')
  8. // =====================================
  9. // PG EXTENSIONS
  10. // =====================================
  11. await knex.raw('CREATE EXTENSION IF NOT EXISTS pgcrypto;')
  12. await knex.schema
  13. // =====================================
  14. // MODEL TABLES
  15. // =====================================
  16. // ANALYTICS ---------------------------
  17. .createTable('analytics', table => {
  18. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  19. table.string('module').notNullable()
  20. table.boolean('isEnabled').notNullable().defaultTo(false)
  21. table.jsonb('config').notNullable()
  22. })
  23. // API KEYS ----------------------------
  24. .createTable('apiKeys', table => {
  25. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  26. table.string('name').notNullable()
  27. table.text('key').notNullable()
  28. table.string('expiration').notNullable()
  29. table.boolean('isRevoked').notNullable().defaultTo(false)
  30. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  31. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  32. })
  33. // ASSETS ------------------------------
  34. .createTable('assets', table => {
  35. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  36. table.string('filename').notNullable()
  37. table.string('hash').notNullable().index()
  38. table.string('ext').notNullable()
  39. table.enum('kind', ['binary', 'image']).notNullable().defaultTo('binary')
  40. table.string('mime').notNullable().defaultTo('application/octet-stream')
  41. table.integer('fileSize').unsigned().comment('In kilobytes')
  42. table.jsonb('metadata')
  43. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  44. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  45. })
  46. // ASSET DATA --------------------------
  47. .createTable('assetData', table => {
  48. table.uuid('id').notNullable().index()
  49. table.binary('data').notNullable()
  50. })
  51. // ASSET FOLDERS -----------------------
  52. .createTable('assetFolders', table => {
  53. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  54. table.string('name').notNullable()
  55. table.string('slug').notNullable()
  56. })
  57. // AUTHENTICATION ----------------------
  58. .createTable('authentication', table => {
  59. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  60. table.string('module').notNullable()
  61. table.boolean('isEnabled').notNullable().defaultTo(false)
  62. table.integer('order').unsigned().notNullable().defaultTo(0)
  63. table.string('displayName').notNullable().defaultTo('')
  64. table.jsonb('config').notNullable().defaultTo('{}')
  65. table.boolean('selfRegistration').notNullable().defaultTo(false)
  66. table.jsonb('domainWhitelist').notNullable().defaultTo('[]')
  67. table.jsonb('autoEnrollGroups').notNullable().defaultTo('[]')
  68. table.jsonb('hideOnSites').notNullable().defaultTo('[]')
  69. })
  70. // COMMENTS ----------------------------
  71. .createTable('comments', table => {
  72. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  73. table.uuid('replyTo')
  74. table.text('content').notNullable()
  75. table.text('render').notNullable().defaultTo('')
  76. table.string('name').notNullable().defaultTo('')
  77. table.string('email').notNullable().defaultTo('')
  78. table.string('ip').notNullable().defaultTo('')
  79. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  80. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  81. })
  82. // GROUPS ------------------------------
  83. .createTable('groups', table => {
  84. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  85. table.string('name').notNullable()
  86. table.jsonb('permissions').notNullable()
  87. table.jsonb('rules').notNullable()
  88. table.string('redirectOnLogin').notNullable().defaultTo('')
  89. table.string('redirectOnFirstLogin').notNullable().defaultTo('')
  90. table.string('redirectOnLogout').notNullable().defaultTo('')
  91. table.boolean('isSystem').notNullable().defaultTo(false)
  92. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  93. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  94. })
  95. // HOOKS -------------------------------
  96. .createTable('hooks', table => {
  97. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  98. table.string('name').notNullable()
  99. table.jsonb('events').notNullable().defaultTo('[]')
  100. table.string('url').notNullable()
  101. table.boolean('includeMetadata').notNullable().defaultTo(false)
  102. table.boolean('includeContent').notNullable().defaultTo(false)
  103. table.boolean('acceptUntrusted').notNullable().defaultTo(false)
  104. table.string('authHeader')
  105. table.enum('state', ['pending', 'error', 'success']).notNullable().defaultTo('pending')
  106. table.string('lastErrorMessage')
  107. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  108. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  109. })
  110. // LOCALES -----------------------------
  111. .createTable('locales', table => {
  112. table.string('code', 5).notNullable().primary()
  113. table.jsonb('strings')
  114. table.boolean('isRTL').notNullable().defaultTo(false)
  115. table.string('name').notNullable()
  116. table.string('nativeName').notNullable()
  117. table.integer('availability').notNullable().defaultTo(0)
  118. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  119. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  120. })
  121. // NAVIGATION ----------------------------
  122. .createTable('navigation', table => {
  123. table.string('key').notNullable().primary()
  124. table.jsonb('config')
  125. })
  126. // PAGE HISTORY ------------------------
  127. .createTable('pageHistory', table => {
  128. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  129. table.uuid('pageId').notNullable().index()
  130. table.string('path').notNullable()
  131. table.string('hash').notNullable()
  132. table.string('title').notNullable()
  133. table.string('description')
  134. table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft')
  135. table.timestamp('publishStartDate')
  136. table.timestamp('publishEndDate')
  137. table.string('action').defaultTo('updated')
  138. table.text('content')
  139. table.string('contentType').notNullable()
  140. table.jsonb('extra').notNullable().defaultTo('{}')
  141. table.jsonb('tags').defaultTo('[]')
  142. table.timestamp('versionDate').notNullable().defaultTo(knex.fn.now())
  143. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  144. })
  145. // PAGE LINKS --------------------------
  146. .createTable('pageLinks', table => {
  147. table.increments('id').primary()
  148. table.string('path').notNullable()
  149. table.string('localeCode', 5).notNullable()
  150. })
  151. // PAGES -------------------------------
  152. .createTable('pages', table => {
  153. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  154. table.string('slug')
  155. table.string('path').notNullable()
  156. table.string('hash').notNullable()
  157. table.string('title').notNullable()
  158. table.string('description')
  159. table.enu('publishState', ['draft', 'published', 'scheduled']).notNullable().defaultTo('draft')
  160. table.timestamp('publishStartDate')
  161. table.timestamp('publishEndDate')
  162. table.text('content')
  163. table.text('render')
  164. table.jsonb('toc')
  165. table.string('contentType').notNullable()
  166. table.jsonb('extra').notNullable().defaultTo('{}')
  167. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  168. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  169. })
  170. // PAGE TREE ---------------------------
  171. .createTable('pageTree', table => {
  172. table.integer('id').unsigned().primary()
  173. table.string('path').notNullable()
  174. table.integer('depth').unsigned().notNullable()
  175. table.string('title').notNullable()
  176. table.boolean('isFolder').notNullable().defaultTo(false)
  177. table.jsonb('ancestors')
  178. })
  179. // RENDERERS ---------------------------
  180. .createTable('renderers', table => {
  181. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  182. table.string('module').notNullable()
  183. table.boolean('isEnabled').notNullable().defaultTo(false)
  184. table.jsonb('config')
  185. })
  186. // SETTINGS ----------------------------
  187. .createTable('settings', table => {
  188. table.string('key').notNullable().primary()
  189. table.jsonb('value')
  190. })
  191. // SITES -------------------------------
  192. .createTable('sites', table => {
  193. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  194. table.string('hostname').notNullable()
  195. table.boolean('isEnabled').notNullable().defaultTo(false)
  196. table.jsonb('config').notNullable()
  197. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  198. })
  199. // STORAGE -----------------------------
  200. .createTable('storage', table => {
  201. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  202. table.string('module').notNullable()
  203. table.boolean('isEnabled').notNullable().defaultTo(false)
  204. table.jsonb('contentTypes')
  205. table.jsonb('assetDelivery')
  206. table.jsonb('versioning')
  207. table.jsonb('schedule')
  208. table.jsonb('config')
  209. table.jsonb('state')
  210. })
  211. // TAGS --------------------------------
  212. .createTable('tags', table => {
  213. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  214. table.string('tag').notNullable()
  215. table.jsonb('display').notNullable().defaultTo('{}')
  216. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  217. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  218. })
  219. // USER AVATARS ------------------------
  220. .createTable('userAvatars', table => {
  221. table.uuid('id').notNullable().primary()
  222. table.binary('data').notNullable()
  223. })
  224. // USER KEYS ---------------------------
  225. .createTable('userKeys', table => {
  226. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  227. table.string('kind').notNullable()
  228. table.string('token').notNullable()
  229. table.timestamp('validUntil').notNullable()
  230. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  231. })
  232. // USERS -------------------------------
  233. .createTable('users', table => {
  234. table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()'))
  235. table.string('email').notNullable()
  236. table.string('name').notNullable()
  237. table.jsonb('auth')
  238. table.jsonb('tfa')
  239. table.jsonb('meta')
  240. table.jsonb('prefs')
  241. table.string('pictureUrl')
  242. table.boolean('isSystem').notNullable().defaultTo(false)
  243. table.boolean('isActive').notNullable().defaultTo(false)
  244. table.boolean('isVerified').notNullable().defaultTo(false)
  245. table.timestamp('lastLoginAt').index()
  246. table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now())
  247. table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now())
  248. })
  249. // =====================================
  250. // RELATION TABLES
  251. // =====================================
  252. // PAGE TAGS ---------------------------
  253. .createTable('pageTags', table => {
  254. table.increments('id').primary()
  255. table.uuid('pageId').references('id').inTable('pages').onDelete('CASCADE')
  256. table.uuid('tagId').references('id').inTable('tags').onDelete('CASCADE')
  257. })
  258. // USER GROUPS -------------------------
  259. .createTable('userGroups', table => {
  260. table.increments('id').primary()
  261. table.uuid('userId').references('id').inTable('users').onDelete('CASCADE')
  262. table.uuid('groupId').references('id').inTable('groups').onDelete('CASCADE')
  263. })
  264. // =====================================
  265. // REFERENCES
  266. // =====================================
  267. .table('analytics', table => {
  268. table.uuid('siteId').notNullable().references('id').inTable('sites')
  269. })
  270. .table('assets', table => {
  271. table.uuid('folderId').notNullable().references('id').inTable('assetFolders').index()
  272. table.uuid('authorId').notNullable().references('id').inTable('users')
  273. table.uuid('siteId').notNullable().references('id').inTable('sites').index()
  274. })
  275. .table('assetFolders', table => {
  276. table.uuid('parentId').references('id').inTable('assetFolders').index()
  277. })
  278. .table('comments', table => {
  279. table.uuid('pageId').notNullable().references('id').inTable('pages').index()
  280. table.uuid('authorId').notNullable().references('id').inTable('users').index()
  281. })
  282. .table('navigation', table => {
  283. table.uuid('siteId').notNullable().references('id').inTable('sites').index()
  284. })
  285. .table('pageHistory', table => {
  286. table.string('localeCode', 5).references('code').inTable('locales')
  287. table.uuid('authorId').notNullable().references('id').inTable('users')
  288. table.uuid('siteId').notNullable().references('id').inTable('sites').index()
  289. })
  290. .table('pageLinks', table => {
  291. table.uuid('pageId').notNullable().references('id').inTable('pages').onDelete('CASCADE')
  292. table.index(['path', 'localeCode'])
  293. })
  294. .table('pages', table => {
  295. table.string('localeCode', 5).references('code').inTable('locales').index()
  296. table.uuid('authorId').notNullable().references('id').inTable('users').index()
  297. table.uuid('creatorId').notNullable().references('id').inTable('users').index()
  298. table.uuid('siteId').notNullable().references('id').inTable('sites').index()
  299. })
  300. .table('pageTree', table => {
  301. table.integer('parent').unsigned().references('id').inTable('pageTree').onDelete('CASCADE')
  302. table.uuid('pageId').notNullable().references('id').inTable('pages').onDelete('CASCADE')
  303. table.string('localeCode', 5).references('code').inTable('locales')
  304. })
  305. .table('storage', table => {
  306. table.uuid('siteId').notNullable().references('id').inTable('sites')
  307. })
  308. .table('tags', table => {
  309. table.uuid('siteId').notNullable().references('id').inTable('sites')
  310. table.unique(['siteId', 'tag'])
  311. })
  312. .table('userKeys', table => {
  313. table.uuid('userId').notNullable().references('id').inTable('users')
  314. })
  315. .table('users', table => {
  316. table.string('localeCode', 5).references('code').inTable('locales').notNullable().defaultTo('en')
  317. })
  318. // =====================================
  319. // DEFAULT DATA
  320. // =====================================
  321. // -> SYSTEM CONFIG
  322. await knex('settings').insert([
  323. {
  324. key: 'mail',
  325. value: {
  326. senderName: '',
  327. senderEmail: '',
  328. host: '',
  329. port: 465,
  330. secure: true,
  331. verifySSL: true,
  332. user: '',
  333. pass: '',
  334. useDKIM: false,
  335. dkimDomainName: '',
  336. dkimKeySelector: '',
  337. dkimPrivateKey: ''
  338. }
  339. },
  340. {
  341. key: 'security',
  342. value: {
  343. corsConfig: '',
  344. corsMode: 'OFF',
  345. cspDirectives: '',
  346. disallowFloc: true,
  347. disallowIframe: true,
  348. disallowOpenRedirect: true,
  349. enforceCsp: false,
  350. enforceHsts: false,
  351. enforceSameOriginReferrerPolicy: true,
  352. forceAssetDownload: true,
  353. hstsDuration: 0,
  354. trustProxy: false,
  355. authJwtAudience: 'urn:wiki.js',
  356. authJwtExpiration: '30m',
  357. authJwtRenewablePeriod: '14d',
  358. uploadMaxFileSize: 10485760,
  359. uploadMaxFiles: 20,
  360. uploadScanSVG: true
  361. }
  362. },
  363. {
  364. key: 'system',
  365. value: {
  366. sessionSecret: crypto.randomBytes(32).toString('hex')
  367. }
  368. },
  369. {
  370. key: 'update',
  371. value: {
  372. locales: true
  373. }
  374. }
  375. ])
  376. // -> DEFAULT LOCALE
  377. await knex('locales').insert({
  378. code: 'en',
  379. strings: {},
  380. isRTL: false,
  381. name: 'English',
  382. nativeName: 'English'
  383. })
  384. // -> DEFAULT SITE
  385. WIKI.logger.info('Generating certificates...')
  386. const secret = crypto.randomBytes(32).toString('hex')
  387. const certs = crypto.generateKeyPairSync('rsa', {
  388. modulusLength: 2048,
  389. publicKeyEncoding: {
  390. type: 'pkcs1',
  391. format: 'pem'
  392. },
  393. privateKeyEncoding: {
  394. type: 'pkcs1',
  395. format: 'pem',
  396. cipher: 'aes-256-cbc',
  397. passphrase: secret
  398. }
  399. })
  400. const siteId = uuid()
  401. await knex('sites').insert({
  402. id: siteId,
  403. hostname: '*',
  404. isEnabled: true,
  405. config: {
  406. auth: {
  407. audience: 'urn:wiki.js',
  408. tokenExpiration: '30m',
  409. tokenRenewal: '14d',
  410. certs: {
  411. jwk: pem2jwk(certs.publicKey),
  412. public: certs.publicKey,
  413. private: certs.privateKey
  414. },
  415. secret
  416. },
  417. title: 'My Wiki Site',
  418. description: '',
  419. company: '',
  420. contentLicense: '',
  421. defaults: {
  422. timezone: 'America/New_York',
  423. dateFormat: 'YYYY-MM-DD',
  424. timeFormat: '12h'
  425. },
  426. features: {
  427. ratings: false,
  428. ratingsMode: 'off',
  429. comments: false,
  430. contributions: false,
  431. profile: true,
  432. search: true
  433. },
  434. logoText: true,
  435. robots: {
  436. index: true,
  437. follow: true
  438. },
  439. locale: 'en',
  440. localeNamespacing: false,
  441. localeNamespaces: [],
  442. theme: {
  443. dark: false,
  444. colorPrimary: '#1976d2',
  445. colorSecondary: '#02c39a',
  446. colorAccent: '#f03a47',
  447. colorHeader: '#000000',
  448. colorSidebar: '#1976d2',
  449. injectCSS: '',
  450. injectHead: '',
  451. injectBody: '',
  452. sidebarPosition: 'left',
  453. tocPosition: 'right',
  454. showSharingMenu: true,
  455. showPrintBtn: true
  456. }
  457. }
  458. })
  459. // -> DEFAULT GROUPS
  460. const groupAdminId = uuid()
  461. const groupGuestId = '10000000-0000-4000-0000-000000000001'
  462. await knex('groups').insert([
  463. {
  464. id: groupAdminId,
  465. name: 'Administrators',
  466. permissions: JSON.stringify(['manage:system']),
  467. rules: JSON.stringify([]),
  468. isSystem: true
  469. },
  470. {
  471. id: groupGuestId,
  472. name: 'Guests',
  473. permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
  474. rules: JSON.stringify([
  475. {
  476. id: uuid(),
  477. name: 'Default Rule',
  478. roles: ['read:pages', 'read:assets', 'read:comments'],
  479. match: 'START',
  480. mode: 'DENY',
  481. path: '',
  482. locales: [],
  483. sites: []
  484. }
  485. ]),
  486. isSystem: true
  487. }
  488. ])
  489. // -> AUTHENTICATION MODULE
  490. const authModuleId = uuid()
  491. await knex('authentication').insert({
  492. id: authModuleId,
  493. module: 'local',
  494. isEnabled: true,
  495. displayName: 'Local Authentication'
  496. })
  497. // -> USERS
  498. const userAdminId = uuid()
  499. const userGuestId = uuid()
  500. await knex('users').insert([
  501. {
  502. id: userAdminId,
  503. email: process.env.ADMIN_EMAIL ?? 'admin@example.com',
  504. auth: {
  505. [authModuleId]: {
  506. password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12),
  507. mustChangePwd: !process.env.ADMIN_PASS,
  508. restrictLogin: false,
  509. tfaRequired: false,
  510. tfaSecret: ''
  511. }
  512. },
  513. name: 'Administrator',
  514. isSystem: false,
  515. isActive: true,
  516. isVerified: true,
  517. meta: {
  518. location: '',
  519. jobTitle: '',
  520. pronouns: ''
  521. },
  522. prefs: {
  523. timezone: 'America/New_York',
  524. dateFormat: 'YYYY-MM-DD',
  525. timeFormat: '12h',
  526. darkMode: false
  527. },
  528. localeCode: 'en'
  529. },
  530. {
  531. id: userGuestId,
  532. email: 'guest@example.com',
  533. name: 'Guest',
  534. isSystem: true,
  535. isActive: true,
  536. isVerified: true,
  537. localeCode: 'en'
  538. }
  539. ])
  540. await knex('userGroups').insert([
  541. {
  542. userId: userAdminId,
  543. groupId: groupAdminId
  544. },
  545. {
  546. userId: userGuestId,
  547. groupId: groupGuestId
  548. }
  549. ])
  550. // -> STORAGE MODULE
  551. await knex('storage').insert({
  552. module: 'db',
  553. siteId,
  554. isEnabled: true,
  555. contentTypes: {
  556. activeTypes: ['pages', 'images', 'documents', 'others', 'large'],
  557. largeThreshold: '5MB'
  558. },
  559. assetDelivery: {
  560. streaming: true,
  561. directAccess: false
  562. },
  563. versioning: {
  564. enabled: false
  565. },
  566. state: {
  567. current: 'ok'
  568. }
  569. })
  570. WIKI.logger.info('Completed 3.0.0 database migration.')
  571. }
  572. exports.down = knex => { }