diff --git a/lib/note/index.js b/lib/note/index.js index 41d8cf587d..4750352ac1 100644 --- a/lib/note/index.js +++ b/lib/note/index.js @@ -8,54 +8,13 @@ const { newCheckViewPermission, errorForbidden, responseCodiMD, errorNotFound, e const { updateHistory, historyDelete } = require('../history') const { actionPublish, actionSlide, actionInfo, actionDownload, actionPDF, actionGist, actionRevision, actionPandoc } = require('./noteActions') const realtime = require('../realtime/realtime') +const service = require('./service') -async function getNoteById (noteId, { includeUser } = { includeUser: false }) { - const id = await Note.parseNoteIdAsync(noteId) - - const includes = [] - - if (includeUser) { - includes.push({ - model: User, - as: 'owner' - }, { - model: User, - as: 'lastchangeuser' - }) - } - - const note = await Note.findOne({ - where: { - id: id - }, - include: includes - }) - return note -} - -async function createNote (userId, noteAlias) { - if (!config.allowAnonymous && !userId) { - throw new Error('can not create note') - } - - const note = await Note.create({ - ownerId: userId, - alias: noteAlias - }) - - if (userId) { - updateHistory(userId, note) - } - - return note -} - -// controller -async function showNote (req, res) { +exports.showNote = async (req, res) => { const noteId = req.params.noteId const userId = req.user ? req.user.id : null - let note = await getNoteById(noteId) + let note = await service.getNote(noteId) if (!note) { // if allow free url enable, auto create note @@ -64,7 +23,7 @@ async function showNote (req, res) { } else if (!config.allowAnonymous && !userId) { return errorForbidden(req, res) } - note = await createNote(userId, noteId) + note = await service.createNote(userId, noteId) } if (!newCheckViewPermission(note, req.isAuthenticated(), userId)) { @@ -79,20 +38,10 @@ async function showNote (req, res) { return responseCodiMD(res, note) } -function canViewNote (note, isLogin, userId) { - if (note.permission === 'private') { - return note.ownerId === userId - } - if (note.permission === 'limited' || note.permission === 'protected') { - return isLogin - } - return true -} - -async function showPublishNote (req, res) { +exports.showPublishNote = async (req, res) => { const shortid = req.params.shortid - const note = await getNoteById(shortid, { + const note = await service.getNote(shortid, { includeUser: true }) @@ -100,12 +49,12 @@ async function showPublishNote (req, res) { return errorNotFound(req, res) } - if (!canViewNote(note, req.isAuthenticated(), req.user ? req.user.id : null)) { + if (!service.canViewNote(note, req.isAuthenticated(), req.user ? req.user.id : null)) { return errorForbidden(req, res) } if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) { - return res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid)) + return res.redirect(config.serviceerURL + '/s/' + (note.alias || note.shortid)) } await note.increment('viewcount') @@ -143,16 +92,16 @@ async function showPublishNote (req, res) { res.render('pretty.ejs', data) } -async function noteActions (req, res) { +exports.noteActions = async (req, res) => { const noteId = req.params.noteId - const note = await getNoteById(noteId) + const note = await service.getNote(noteId) if (!note) { return errorNotFound(req, res) } - if (!canViewNote(note, req.isAuthenticated(), req.user ? req.user.id : null)) { + if (!service.canViewNote(note, req.isAuthenticated(), req.user ? req.user.id : null)) { return errorForbidden(req, res) } @@ -187,41 +136,13 @@ async function noteActions (req, res) { actionPandoc(req, res, note) break default: - return res.redirect(config.serverURL + '/' + noteId) - } -} - -async function getMyNoteList (userId, callback) { - const myNotes = await Note.findAll({ - where: { - ownerId: userId - } - }) - if (!myNotes) { - return callback(null, null) - } - try { - const myNoteList = myNotes.map(note => ({ - id: Note.encodeNoteId(note.id), - text: note.title, - tags: Note.parseNoteInfo(note.content).tags, - createdAt: note.createdAt, - lastchangeAt: note.lastchangeAt, - shortId: note.shortid - })) - if (config.debug) { - logger.info('Parse myNoteList success: ' + userId) - } - return callback(null, myNoteList) - } catch (err) { - logger.error('Parse myNoteList failed') - return callback(err, null) + return res.redirect(config.serviceerURL + '/' + noteId) } } -function listMyNotes (req, res) { +exports.listMyNotes = (req, res) => { if (req.isAuthenticated()) { - getMyNoteList(req.user.id, (err, myNoteList) => { + service.getMyNoteList(req.user.id, (err, myNoteList) => { if (err) return errorInternalError(req, res) if (!myNoteList) return errorNotFound(req, res) res.send({ @@ -233,7 +154,7 @@ function listMyNotes (req, res) { } } -const deleteNote = async (req, res) => { +exports.deleteNote = async (req, res) => { if (req.isAuthenticated()) { const noteId = await Note.parseNoteIdAsync(req.params.noteId) try { @@ -267,7 +188,7 @@ const deleteNote = async (req, res) => { } } -const updateNote = async (req, res) => { +exports.updateNote = async (req, res) => { if (req.isAuthenticated() || config.allowAnonymousEdits) { const noteId = await Note.parseNoteIdAsync(req.params.noteId) try { @@ -332,9 +253,43 @@ const updateNote = async (req, res) => { } } -exports.showNote = showNote -exports.showPublishNote = showPublishNote -exports.noteActions = noteActions -exports.listMyNotes = listMyNotes -exports.deleteNote = deleteNote -exports.updateNote = updateNote +exports.updateNoteAlias = async (req, res) => { + const originAliasOrNoteId = req.params.originAliasOrNoteId + const alias = req.body.alias || '' + const userId = req.user ? req.user.id : null + const note = await service.getNote(originAliasOrNoteId) + .catch((err) => { + logger.error('get note failed:' + err.message) + return false + }) + + if (!note) { + logger.error('update note alias failed: note not found.') + return res.status(404).json({ status: 'error', message: 'Not found' }) + } + + if (note.ownerId !== userId) { + return res.status(403).json({ status: 'error', message: 'Forbidden' }) + } + + const result = await service.updateAlias(originAliasOrNoteId, alias) + .catch((err) => { + logger.error('update note alias failed:' + err.message) + return false + }) + + if (!result) { + return res.status(500).json({ status: 'error', message: 'Internal Error' }) + } + + const { isSuccess, errorMessage } = result + + if (!isSuccess) { + res.status(400).json({ status: 'error', message: errorMessage }) + return + } + + res.send({ + status: 'ok' + }) +} diff --git a/lib/note/service.js b/lib/note/service.js new file mode 100644 index 0000000000..4e74d98bc1 --- /dev/null +++ b/lib/note/service.js @@ -0,0 +1,150 @@ + +'use strict' +const { Note, User } = require('../models') +const config = require('../config') +const logger = require('../logger') +const realtime = require('../realtime/realtime') +const { updateHistory } = require('../history') + +const EXIST_WEB_ROUTES = ['', 'new', 'me', 'history', '403', '404', '500', 'config'] +const FORBIDDEN_ALIASES = [...EXIST_WEB_ROUTES, ...config.forbiddenNoteIDs] + +exports.getNote = async (originAliasOrNoteId, { includeUser } = { includeUser: false }) => { + const id = await Note.parseNoteIdAsync(originAliasOrNoteId) + + const includes = [] + + if (includeUser) { + includes.push({ + model: User, + as: 'owner' + }, { + model: User, + as: 'lastchangeuser' + }) + } + + const note = await Note.findOne({ + where: { + id: id + }, + include: includes + }) + return note +} + +exports.createNote = async (userId, noteAlias) => { + if (!config.allowAnonymous && !userId) { + throw new Error('can not create note') + } + + const note = await Note.create({ + ownerId: userId, + alias: noteAlias + }) + + if (userId) { + updateHistory(userId, note) + } + + return note +} + +exports.canViewNote = (note, isLogin, userId) => { + if (note.permission === 'private') { + return note.ownerId === userId + } + if (note.permission === 'limited' || note.permission === 'protected') { + return isLogin + } + return true +} + +exports.getMyNoteList = async (userId, callback) => { + const myNotes = await Note.findAll({ + where: { + ownerId: userId + } + }) + if (!myNotes) { + return callback(null, null) + } + try { + const myNoteList = myNotes.map(note => ({ + id: Note.encodeNoteId(note.id), + text: note.title, + tags: Note.parseNoteInfo(note.content).tags, + createdAt: note.createdAt, + lastchangeAt: note.lastchangeAt, + shortId: note.shortid + })) + if (config.debug) { + logger.info('Parse myNoteList success: ' + userId) + } + return callback(null, myNoteList) + } catch (err) { + logger.error('Parse myNoteList failed') + return callback(err, null) + } +} + +const sanitizeAlias = (alias) => { + return alias.replace(/( |\/)/g, '') +} + +const checkAliasValid = async (originAliasOrNoteId, alias) => { + const sanitizedAlias = sanitizeAlias(alias) + if (FORBIDDEN_ALIASES.includes(sanitizedAlias)) { + return { + isValid: false, + errorMessage: 'The url is exist.' + } + } + + if (!/^[0-9a-z-_]+$/.test(alias)) { + return { + isValid: false, + errorMessage: 'The url must be lowercase letters, decimal digits, hyphen or underscore.' + } + } + + const conflictNote = await exports.getNote(alias) + const note = await exports.getNote(originAliasOrNoteId) + + if (conflictNote && conflictNote.id !== note.id) { + return { + isValid: false, + errorMessage: 'The url is exist.' + } + } + + return { + isValid: true + } +} + +exports.updateAlias = async (originAliasOrNoteId, alias) => { + const sanitizedAlias = sanitizeAlias(alias) + const note = await exports.getNote(originAliasOrNoteId) + const { isValid, errorMessage } = await checkAliasValid(originAliasOrNoteId, alias) + if (!isValid) { + return { + isSuccess: false, + errorMessage + } + } + + const updatedNote = await note.update({ + alias: sanitizedAlias, + lastchangeAt: Date.now() + }) + + realtime.io.to(updatedNote.id) + .emit('alias updated', { + alias: updatedNote.alias + }) + + return { + isSuccess: true + } +} diff --git a/lib/response.js b/lib/response.js index 37a955ba66..898782e6a6 100644 --- a/lib/response.js +++ b/lib/response.js @@ -79,7 +79,8 @@ function responseCodiMD (res, note) { 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling }) res.render('codimd.ejs', { - title: title + title: title, + alias: note.alias }) } diff --git a/lib/routes.js b/lib/routes.js index 9d15c3499b..bc204c0cd2 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -71,7 +71,9 @@ appRouter.get('/s/:shortid/:action', response.publishNoteActions) appRouter.get('/p/:shortid', response.showPublishSlide) // publish slide actions appRouter.get('/p/:shortid/:action', response.publishSlideActions) -// gey my note list +// update note alias +appRouter.patch('/api/notes/:originAliasOrNoteId/alias', bodyParser.json(), noteController.updateNoteAlias) +// get my note list appRouter.get('/api/notes/myNotes', noteController.listMyNotes) // delete note by id appRouter.delete('/api/notes/:noteId', noteController.deleteNote) diff --git a/locales/en.json b/locales/en.json index b2883192f1..8359aa4419 100644 --- a/locales/en.json +++ b/locales/en.json @@ -117,5 +117,7 @@ "Powered by %s": "Powered by %s", "Register": "Register", "Export with pandoc": "Export with pandoc", - "Select output format": "Select output format" + "Select output format": "Select output format", + "Note alias": "Note alias", + "Submit": "Submit" } \ No newline at end of file diff --git a/locales/zh-TW.json b/locales/zh-TW.json index e6c1951a02..0704089fdd 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -117,5 +117,7 @@ "Powered by %s": "技術支援:%s", "Register": "註冊", "Export with pandoc": "使用 pandoc 匯出", - "Select output format": "選擇輸出格式" + "Select output format": "選擇輸出格式", + "Note alias": "筆記別名", + "Submit": "送出" } diff --git a/public/js/index.js b/public/js/index.js index 7f4f576ec7..1ac62a4bec 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -31,8 +31,10 @@ import { DROPBOX_APP_KEY, noteid, noteurl, + noteAlias, urlpath, - version + version, + updateNoteAliasConfig } from './lib/config' import { @@ -1234,6 +1236,64 @@ $('#revisionModalRevert').click(function () { editor.setValue(revision.content) ui.modal.revision.modal('hide') }) + +// custom note url modal +const updateNoteAlias = (newAlias = '') => { + return new Promise((resolve, reject) => { + $.ajax({ + method: 'PATCH', + url: `/api/notes/${noteid}/alias`, + dataType: 'json', + contentType: 'application/json;charset=utf-8', + data: JSON.stringify({ + alias: newAlias + }), + success: resolve, + error: reject + }) + }) +} + +ui.modal.changeNoteAliasModal.on('show.bs.modal', function (e) { + ui.modal.changeNoteAliasModal.find('[name="note-alias"]').val(noteAlias) +}) + +ui.modal.changeNoteAliasModal.on('submit', function (e) { + e.preventDefault() + const showErrorMessage = (msg) => { + ui.modal.changeNoteAliasModal.find('.js-error-message').text(msg) + ui.modal.changeNoteAliasModal.find('.js-error-alert').show() + } + const hideErrorMessage = () => ui.modal.changeNoteAliasModal.find('.js-error-alert').hide() + + const newNoteAlias = ui.modal.changeNoteAliasModal.find('[name="note-alias"]').val() + if (!/^[0-9a-z-_]+$/.test(newNoteAlias)) { + showErrorMessage('The url must be lowercase letters, decimal digits, hyphen or underscore.') + return + } + + updateNoteAlias(newNoteAlias) + .then( + ({ status }) => { + if (status === 'ok') { + hideErrorMessage() + ui.modal.changeNoteAliasModal.modal('hide') + } + } + ) + .catch((err) => { + if (err.status === 400 && err.responseJSON.message) { + showErrorMessage(err.responseJSON.message) + return + } + if (err.status === 403) { + showErrorMessage('Only note owner can edit custom url.') + return + } + showErrorMessage('Something wrong.') + }) +}) + // snippet projects ui.modal.snippetImportProjects.change(function () { var accesstoken = $('#snippetImportModalAccessToken').val() @@ -1764,6 +1824,7 @@ socket.on('version', function (data) { } } }) + var authors = [] var authorship = [] var authorMarks = {} // temp variable @@ -2178,6 +2239,12 @@ socket.on('cursor blur', function (data) { } }) +socket.on('alias updated', function (data) { + const alias = data.alias + history.replaceState({}, '', alias) + updateNoteAliasConfig(alias) +}) + var options = { valueNames: ['id', 'name'], item: '
  • ' + diff --git a/public/js/lib/config/index.js b/public/js/lib/config/index.js index 6133e2c86f..ed69825dbf 100644 --- a/public/js/lib/config/index.js +++ b/public/js/lib/config/index.js @@ -1,13 +1,26 @@ export const DROPBOX_APP_KEY = window.DROPBOX_APP_KEY || '' - export const domain = window.domain || '' // domain name export const urlpath = window.urlpath || '' // sub url path, like: www.example.com/ export const debug = window.debug || false - export const port = window.location.port export const serverurl = `${window.location.protocol}//${domain || window.location.hostname}${port ? ':' + port : ''}${urlpath ? '/' + urlpath : ''}` window.serverurl = serverurl -export const noteid = decodeURIComponent(urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1]) -export const noteurl = `${serverurl}/${noteid}` +export let noteid = '' +export let noteurl = '' +export let noteAlias = document.querySelector("meta[name='note-alias']").getAttribute('content') + +export function updateNoteAliasConfig (alias) { + noteAlias = alias + document.querySelector("meta[name='note-alias']").setAttribute('content', noteAlias) + + refreshNoteUrlConfig() +} + +export function refreshNoteUrlConfig () { + noteid = decodeURIComponent(urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1]) + noteurl = `${serverurl}/${noteid}` +} + +refreshNoteUrlConfig() export const version = window.version diff --git a/public/js/lib/editor/ui-elements.js b/public/js/lib/editor/ui-elements.js index bedf51fcba..3b6fa94381 100644 --- a/public/js/lib/editor/ui-elements.js +++ b/public/js/lib/editor/ui-elements.js @@ -88,7 +88,8 @@ export const getUIElements = () => ({ snippetImportProjects: $('#snippetImportModalProjects'), snippetImportSnippets: $('#snippetImportModalSnippets'), revision: $('#revisionModal'), - pandocExport: $('.pandoc-export-modal') + pandocExport: $('.pandoc-export-modal'), + changeNoteAliasModal: $('#noteAliasModal') } }) diff --git a/public/views/codimd/body.ejs b/public/views/codimd/body.ejs index 9d5894c4c5..80da4ecef0 100644 --- a/public/views/codimd/body.ejs +++ b/public/views/codimd/body.ejs @@ -262,3 +262,4 @@ <%- include('../shared/help-modal') %> <%- include('../shared/revision-modal') %> <%- include('../shared/pandoc-export-modal') %> +<%- include('../shared/change-note-alias-modal') %> diff --git a/public/views/codimd/head.ejs b/public/views/codimd/head.ejs index 4d59ced1b7..896c20dddb 100644 --- a/public/views/codimd/head.ejs +++ b/public/views/codimd/head.ejs @@ -4,6 +4,7 @@ + <%= title %> diff --git a/public/views/codimd/header.ejs b/public/views/codimd/header.ejs index 79ea50032e..8a93898da4 100644 --- a/public/views/codimd/header.ejs +++ b/public/views/codimd/header.ejs @@ -31,6 +31,8 @@
  • +
  • <%= __('Note alias') %> +
  • <%= __('Revision') %>
  • <%= __('Slide Mode') %> @@ -137,6 +139,8 @@