mirror of https://github.com/status-im/codimd.git
create custom-note-url flow
Signed-off-by: nick.chen <nick.chen.sudo@gmail.com>
This commit is contained in:
parent
1bdedf17b6
commit
1579f20ccc
|
@ -0,0 +1,32 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
return queryInterface.createTable('ArchivedNoteAliases', {
|
||||
id: {
|
||||
type: Sequelize.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: Sequelize.UUIDV4
|
||||
},
|
||||
noteId: Sequelize.UUID,
|
||||
alias: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
createdAt: Sequelize.DATE,
|
||||
updatedAt: Sequelize.DATE
|
||||
})
|
||||
.then(
|
||||
() => queryInterface.addIndex(
|
||||
'ArchivedNoteAliases',
|
||||
['alias'], {
|
||||
indicesType: 'UNIQUE'
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
down: function (queryInterface) {
|
||||
return queryInterface.dropTable('Users');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
const Sequelize = require('sequelize')
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
const ArchivedNoteAlias = sequelize.define('ArchivedNoteAlias', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: Sequelize.UUIDV4
|
||||
},
|
||||
noteId: {
|
||||
type: DataTypes.UUID
|
||||
},
|
||||
alias: {
|
||||
type: DataTypes.STRING,
|
||||
unique: true
|
||||
}
|
||||
})
|
||||
|
||||
ArchivedNoteAlias.associate = function (models) {
|
||||
ArchivedNoteAlias.belongsTo(models.Note, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'note',
|
||||
constraints: false
|
||||
})
|
||||
}
|
||||
|
||||
return ArchivedNoteAlias
|
||||
}
|
|
@ -157,6 +157,11 @@ module.exports = function (sequelize, DataTypes) {
|
|||
as: 'authors',
|
||||
constraints: false
|
||||
})
|
||||
Note.hasMany(models.ArchivedNoteAlias, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'archivedAlias',
|
||||
constraints: false
|
||||
})
|
||||
}
|
||||
Note.checkFileExist = function (filePath) {
|
||||
try {
|
||||
|
@ -223,6 +228,18 @@ module.exports = function (sequelize, DataTypes) {
|
|||
|
||||
Note.parseNoteId = function (noteId, callback) {
|
||||
async.series({
|
||||
parseNoteIdByArchivedAlias: function (_callback) {
|
||||
sequelize.models.ArchivedNoteAlias.findOne({
|
||||
where: {
|
||||
alias: noteId
|
||||
}
|
||||
}).then(function (archivedAlias) {
|
||||
if (!archivedAlias) {
|
||||
return _callback(null, null)
|
||||
}
|
||||
return callback(null, archivedAlias.noteId)
|
||||
});
|
||||
},
|
||||
parseNoteIdByAlias: function (_callback) {
|
||||
// try to parse note id by alias (e.g. doc)
|
||||
Note.findOne({
|
||||
|
|
|
@ -4,10 +4,11 @@ const config = require('../config')
|
|||
const logger = require('../logger')
|
||||
const { Note, User, Revision } = require('../models')
|
||||
|
||||
const { newCheckViewPermission, errorForbidden, responseCodiMD, errorNotFound, errorInternalError } = require('../response')
|
||||
const { newCheckViewPermission, errorForbidden, responseCodiMD, errorNotFound, errorInternalError, responseError } = require('../response')
|
||||
const { updateHistory, historyDelete } = require('../history')
|
||||
const { actionPublish, actionSlide, actionInfo, actionDownload, actionPDF, actionGist, actionRevision, actionPandoc } = require('./noteActions')
|
||||
const realtime = require('../realtime/realtime')
|
||||
const serv = require('./service');
|
||||
|
||||
async function getNoteById (noteId, { includeUser } = { includeUser: false }) {
|
||||
const id = await Note.parseNoteIdAsync(noteId)
|
||||
|
@ -332,9 +333,66 @@ const updateNote = async (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
const checkAliasValid = async (req, res) => {
|
||||
const originAliasOrNoteId = req.params.originAliasOrNoteId;
|
||||
const alias = req.query.alias;
|
||||
const isValid = await serv.asyncCheckAliasValid(originAliasOrNoteId, alias)
|
||||
.catch((err) => {
|
||||
logger.error(err.message);
|
||||
return res.status(500).send('Internal Error.');
|
||||
})
|
||||
|
||||
res.send({
|
||||
status: 'ok',
|
||||
isValid
|
||||
});
|
||||
}
|
||||
|
||||
const 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 serv.asyncGetNote(originAliasOrNoteId)
|
||||
.catch((err) => {
|
||||
logger.error('get note failed:' + err.message);
|
||||
return false;
|
||||
})
|
||||
|
||||
if (note.ownerId != userId) {
|
||||
return res.status(403).send('Forbidden.');
|
||||
}
|
||||
|
||||
const isValid = await serv.asyncCheckAliasValid(originAliasOrNoteId, alias)
|
||||
.catch((err) => {
|
||||
logger.error(err.message);
|
||||
return res.status(500).send('Internal Error.');
|
||||
})
|
||||
|
||||
if (!isValid) {
|
||||
console.log("\n\n\n", err.message, "\n\n\n");
|
||||
return res.status(400).send('Bad Request.');
|
||||
}
|
||||
|
||||
const isSuccess = await serv.asyncUpdateAlias(originAliasOrNoteId, alias)
|
||||
.catch((err) => {
|
||||
logger.error('update note alias failed:' + err.message);
|
||||
return false;
|
||||
})
|
||||
|
||||
if (!isSuccess) {
|
||||
return res.status(500).send('Internal Error.');
|
||||
}
|
||||
|
||||
res.send({
|
||||
status: 'ok'
|
||||
})
|
||||
}
|
||||
|
||||
exports.showNote = showNote
|
||||
exports.showPublishNote = showPublishNote
|
||||
exports.noteActions = noteActions
|
||||
exports.listMyNotes = listMyNotes
|
||||
exports.deleteNote = deleteNote
|
||||
exports.updateNote = updateNote
|
||||
exports.checkAliasValid = checkAliasValid
|
||||
exports.updateNoteAlias = updateNoteAlias
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
|
||||
const { Note, ArchivedNoteAlias, sequelize } = require('../models');
|
||||
const realtime = require('../realtime/realtime');
|
||||
|
||||
const forbiddenAlias = ['', 'new', 'me', 'history', '403', '404', '500', 'config'];
|
||||
const sanitize = (alias) => {
|
||||
return alias.replace(/( |\/)/, '');
|
||||
}
|
||||
|
||||
const asyncGetNote = async (originAliasOrNoteId) => {
|
||||
const noteId = await Note.parseNoteIdAsync(originAliasOrNoteId);
|
||||
const note = await Note.findOne({
|
||||
where: {
|
||||
id: noteId
|
||||
}
|
||||
})
|
||||
if (!note) {
|
||||
throw Error('Can\'t find the note.');
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
const asyncGetNoteIdForAliasConflict = async (alias) => {
|
||||
const sanitizedAlias = sanitize(alias);
|
||||
const p1 = Note.findOne({
|
||||
where: {
|
||||
alias: sanitizedAlias
|
||||
}
|
||||
});
|
||||
const p2 = ArchivedNoteAlias.findOne({
|
||||
where: {
|
||||
alias: sanitizedAlias
|
||||
}
|
||||
});
|
||||
const [conflictNote, conflictAarchivedAlias] = await Promise.all([p1, p2]);
|
||||
|
||||
if (conflictNote) {
|
||||
return conflictNote.id
|
||||
}
|
||||
if (conflictAarchivedAlias) {
|
||||
return conflictAarchivedAlias.noteId
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const asyncCheckAliasValid = async (originAliasOrNoteId, alias) => {
|
||||
const sanitizedAlias = sanitize(alias);
|
||||
if (forbiddenAlias.indexOf(sanitizedAlias) > -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const conflictNoteId = await asyncGetNoteIdForAliasConflict(alias)
|
||||
.catch((err) => { throw err });
|
||||
|
||||
const note = await asyncGetNote(originAliasOrNoteId)
|
||||
.catch((err) => { throw err });
|
||||
|
||||
return !conflictNoteId || conflictNoteId === note.id;
|
||||
}
|
||||
|
||||
const asyncUpdateAlias = async (originAliasOrNoteId, alias) => {
|
||||
const sanitizedAlias = sanitize(alias);
|
||||
const note = await asyncGetNote(originAliasOrNoteId)
|
||||
.catch((err) => { throw err });
|
||||
|
||||
const t = await sequelize.transaction();
|
||||
if (note.alias) {
|
||||
const archivedAlias = await ArchivedNoteAlias.findOne({
|
||||
where: {
|
||||
alias: note.alias,
|
||||
}
|
||||
})
|
||||
.catch(async err => { throw err })
|
||||
if (!archivedAlias) {
|
||||
await ArchivedNoteAlias.create({
|
||||
noteId: note.id,
|
||||
alias: note.alias
|
||||
}, { transaction: t })
|
||||
.catch(async err => {
|
||||
await t.rollback();
|
||||
throw Error('Add archived note alias failed. ' + err.message);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updatedNote = await note.update({
|
||||
alias: sanitizedAlias,
|
||||
lastchangeAt: Date.now()
|
||||
}, { transaction: t })
|
||||
.catch(async err => {
|
||||
await t.rollback();
|
||||
throw Error('Write note content error. ' + err.message);
|
||||
})
|
||||
|
||||
await t.commit();
|
||||
|
||||
realtime.io.to(updatedNote.id)
|
||||
.emit('alias updated', {
|
||||
alias: updatedNote.alias
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
exports.sanitize = sanitize;
|
||||
exports.asyncGetNote = asyncGetNote;
|
||||
exports.asyncCheckAliasValid = asyncCheckAliasValid;
|
||||
exports.asyncUpdateAlias = asyncUpdateAlias;
|
|
@ -71,7 +71,11 @@ 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
|
||||
// check note alais valid
|
||||
appRouter.get('/api/notes/:originAliasOrNoteId/checkAliasValid', noteController.checkAliasValid)
|
||||
// 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)
|
||||
|
|
|
@ -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",
|
||||
"Custom Note Url": "Custom Note Url",
|
||||
"Submit": "Submit"
|
||||
}
|
|
@ -117,5 +117,7 @@
|
|||
"Powered by %s": "技術支援:%s",
|
||||
"Register": "註冊",
|
||||
"Export with pandoc": "使用 pandoc 匯出",
|
||||
"Select output format": "選擇輸出格式"
|
||||
"Select output format": "選擇輸出格式",
|
||||
"Custom Note Url": "自訂筆記網址",
|
||||
"Submit": "送出"
|
||||
}
|
||||
|
|
|
@ -1266,6 +1266,73 @@ $('#revisionModalRevert').click(function () {
|
|||
editor.setValue(revision.content)
|
||||
ui.modal.revision.modal('hide')
|
||||
})
|
||||
|
||||
// custom note url modal
|
||||
const checkNoteUrlValid = (noteUrl = '') => {
|
||||
return new Promise((resolve) => {
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
url: `/api/notes/${noteid}/checkAliasValid`,
|
||||
data: {
|
||||
alias: noteUrl
|
||||
},
|
||||
success: (data) => {
|
||||
resolve(data.isValid);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const updateNoteUrl = (noteUrl = '') => {
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
method: 'PATCH',
|
||||
url: `/api/notes/${noteid}/alias`,
|
||||
data: JSON.stringify({
|
||||
alias: noteUrl
|
||||
}),
|
||||
contentType: "application/json;charset=utf-8",
|
||||
success: (data) => {
|
||||
resolve(data.status === 'ok');
|
||||
},
|
||||
error: reject
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
ui.modal.customNoteUrl.on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const showErrorMessage = (msg) => {
|
||||
ui.modal.customNoteUrl.find('.error-message').text(msg);
|
||||
ui.modal.customNoteUrl.find('.alert').show();
|
||||
}
|
||||
const hideErrorMessage = () => ui.modal.customNoteUrl.find('.alert').hide();
|
||||
|
||||
const customUrl = ui.modal.customNoteUrl.find('[name="custom-url"]').val();
|
||||
checkNoteUrlValid(customUrl)
|
||||
.then(isValid => {
|
||||
if (!isValid) {
|
||||
showErrorMessage('The url is exist.');
|
||||
return ;
|
||||
}
|
||||
hideErrorMessage();
|
||||
return updateNoteUrl(customUrl);
|
||||
})
|
||||
.then(isSuccess => {
|
||||
if(isSuccess){
|
||||
hideErrorMessage();
|
||||
ui.modal.customNoteUrl.modal('hide')
|
||||
}
|
||||
}, err => {
|
||||
if(err.status == 403){
|
||||
showErrorMessage('Only note owner can edit custom url.');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
showErrorMessage('Something wrong: ' + err.message);
|
||||
})
|
||||
})
|
||||
|
||||
// snippet projects
|
||||
ui.modal.snippetImportProjects.change(function () {
|
||||
var accesstoken = $('#snippetImportModalAccessToken').val()
|
||||
|
@ -1803,6 +1870,7 @@ socket.on('version', function (data) {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
var authors = []
|
||||
var authorship = []
|
||||
var authorMarks = {} // temp variable
|
||||
|
@ -2217,6 +2285,11 @@ socket.on('cursor blur', function (data) {
|
|||
}
|
||||
})
|
||||
|
||||
socket.on('alias updated', function (data) {
|
||||
const alias = data.alias;
|
||||
history.replaceState({}, '', alias)
|
||||
});
|
||||
|
||||
var options = {
|
||||
valueNames: ['id', 'name'],
|
||||
item: '<li class="ui-user-item">' +
|
||||
|
|
|
@ -83,7 +83,8 @@ export const getUIElements = () => ({
|
|||
snippetImportProjects: $('#snippetImportModalProjects'),
|
||||
snippetImportSnippets: $('#snippetImportModalSnippets'),
|
||||
revision: $('#revisionModal'),
|
||||
pandocExport: $('.pandoc-export-modal')
|
||||
pandocExport: $('.pandoc-export-modal'),
|
||||
customNoteUrl: $('#customNoteUrlModal')
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -251,3 +251,4 @@
|
|||
<%- include ../shared/help-modal %>
|
||||
<%- include ../shared/revision-modal %>
|
||||
<%- include ../shared/pandoc-export-modal %>
|
||||
<%- include ../shared/custom-note-url-modal %>
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header"><%= __('Extra') %></li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-extra-revision" tabindex="-1" data-toggle="modal" data-target="#customNoteUrlModal"><i class="fa fa-pencil-square-o fa-fw"></i> <%= __('Custom Note Url') %></a>
|
||||
</li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-extra-revision" tabindex="-1" data-toggle="modal" data-target="#revisionModal"><i class="fa fa-history fa-fw"></i> <%= __('Revision') %></a>
|
||||
</li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank" rel="noopener"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a>
|
||||
|
@ -132,6 +134,8 @@
|
|||
</a>
|
||||
<ul class="dropdown-menu list" role="menu" aria-labelledby="menu">
|
||||
<li class="dropdown-header"><%= __('Extra') %></li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-extra-revision" tabindex="-1" data-toggle="modal" data-target="#customNoteUrlModal"><i class="fa fa-pencil-square-o fa-fw"></i> <%= __('Custom Note Url') %></a>
|
||||
</li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-extra-revision" tabindex="-1" data-toggle="modal" data-target="#revisionModal"><i class="fa fa-history fa-fw"></i> <%= __('Revision') %></a>
|
||||
</li>
|
||||
<li role="presentation"><a role="menuitem" class="ui-extra-slide" tabindex="-1" href="#" target="_blank" rel="noopener"><i class="fa fa-tv fa-fw"></i> <%= __('Slide Mode') %></a>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<!-- custom-note-url-modal -->
|
||||
<div class="modal fade" id="customNoteUrlModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form role="form">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
|
||||
aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title"><i class="fa fa-pencil-square-o"></i>
|
||||
<%= __('Custom Note Url') %>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<div class="alert alert-danger" style="display: none" role="alert">
|
||||
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
|
||||
<span class="error-message"></span>
|
||||
</div>
|
||||
<p>For example: /hello-world</p>
|
||||
<input type="text" class="form-control" name="custom-url" placeholder="hello-world" required>
|
||||
<div class="help-block with-errors"></div>
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<button type="submit" class="btn btn-primary" style="margin-left: auto;">
|
||||
<%= __('Submit') %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue