create custom-note-url flow

Signed-off-by: nick.chen <nick.chen.sudo@gmail.com>
This commit is contained in:
nick.chen 2021-06-20 08:01:41 +08:00
parent 1bdedf17b6
commit 1579f20ccc
13 changed files with 368 additions and 5 deletions

View File

@ -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');
}
}

View File

@ -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
}

View File

@ -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({

View File

@ -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

108
lib/note/service.js Normal file
View File

@ -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;

View File

@ -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)

View File

@ -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"
}

View File

@ -117,5 +117,7 @@
"Powered by %s": "技術支援:%s",
"Register": "註冊",
"Export with pandoc": "使用 pandoc 匯出",
"Select output format": "選擇輸出格式"
"Select output format": "選擇輸出格式",
"Custom Note Url": "自訂筆記網址",
"Submit": "送出"
}

View File

@ -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">' +

View File

@ -83,7 +83,8 @@ export const getUIElements = () => ({
snippetImportProjects: $('#snippetImportModalProjects'),
snippetImportSnippets: $('#snippetImportModalSnippets'),
revision: $('#revisionModal'),
pandocExport: $('.pandoc-export-modal')
pandocExport: $('.pandoc-export-modal'),
customNoteUrl: $('#customNoteUrlModal')
}
})

View File

@ -251,3 +251,4 @@
<%- include ../shared/help-modal %>
<%- include ../shared/revision-modal %>
<%- include ../shared/pandoc-export-modal %>
<%- include ../shared/custom-note-url-modal %>

View File

@ -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>

View File

@ -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">&times;</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>