Skip to content

Commit

Permalink
build pkg, public projects, logout, mime detect, start wizard, readme
Browse files Browse the repository at this point in the history
  • Loading branch information
Jes committed May 26, 2018
1 parent be93f23 commit 4aa47ff
Show file tree
Hide file tree
Showing 14 changed files with 205 additions and 101 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ package-lock.json
node_modules
projects.json
sessions
build
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@
### Features
* file browser (create, copy, rename, delete, download, upload and preview)
* tabs for opened files
* instant files saving
* instant files saving and watching for changes
* visual diff for repository enabled projects (git)
* marking of events for shared users actions (cursor, selection, edition)
* user authentification per every project

used the best multifunction web editor [ACE](https://ace.c9.io/) ever

### Installation
the [GIT](https://git-scm.com/) is strongly recomended to install as dependency
### Binary installation
download https://github.com/akaJes/jesed/releases and run

enter your credentials and open http://localhost:3000/
### NPM installation
```
mkdir jesed ; cd jesed
npm i jesed
./jesed
```
the [GIT](https://git-scm.com/) is strongly recomended to install as dependency
enter your credentials and open http://localhost:3000/

#### Create new User
`./jesed user add me`
#### Add/modify projects
Expand Down
20 changes: 20 additions & 0 deletions app/mime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const fs = require("fs");
const path = require("path");

const ft = require('file-type');
const isBinary = require('is-binary');
const mt = require('mime-types');

const {promisify} = require('./helpers');

module.exports = (name) =>
promisify(fs.stat)(name)
.then(stats => Promise.all([
stats,
new Buffer(Math.min(stats.size, 512)),
!stats.isDirectory() && promisify(fs.open)(name, 'r'),
]))
.then(a => a[2] && promisify(fs.read)(a[2], a[1], 0, a[1].length, null).then(f => a) || a)
.then(a => [a[0], path.parse(name).ext, ft(a[1]), isBinary(a[1].toString()), mt.lookup(path.parse(name).base)])
.then(a => a.slice(0, 2).concat(a[2] && a[2].mime || a[3] && 'application/octet-stream' || 'text/plain'))
.then(a => ({exts: a[1], mime: a[2], stats: a[0]}))
13 changes: 9 additions & 4 deletions app/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ passport.use(new Strategy({realm: 'Users', qop: 'auth' }, //password
return done(null, true);
}*/
));

app.use(session({
// store: new FileStore({path: path.join(process.cwd(), 'sessions')}),
store: new FileStore(),
secret: 'lazzy keyboard cat',
resave: false,
Expand All @@ -51,7 +53,6 @@ app.use(session({

app.use(passport.initialize());
app.use(passport.session());
//app.get('/logout', function(req, res) { res.redirect('/') }) //'//' + req.headers.host +
const modAuth = (req, res, next) => {
const url = req.url;
const auth = passport.authenticate('digest', { session: true });
Expand All @@ -62,17 +63,18 @@ const modAuth = (req, res, next) => {
})
}
app.use(modAuth);//, passport.authenticate('digest', { session: true }));
const checkAccess = (p, u) => p && u && p.editors && (p.editors.indexOf(u) >= 0 || p.editors.indexOf("*") >= 0);
const auth = p => (req, res, next) => {
req.project = p;
req.session.tokens || (req.session.tokens = {});
if (req.isAuthenticated() && p.editors.indexOf(req.user.id) >= 0) {
if (req.isAuthenticated() && checkAccess(p, req.user.id)) {
tokens[p.id][req.sessionID] || (req.session.tokens[p.id] = tokens[p.id][req.sessionID] = {user: req.user.id, token: md5(req.sessionID + new Date().getTime())});
next();
} else
res.redirect('/');
}

app.use('/nm', express.static(path.join('node_modules')));
app.use('/nm', express.static(path.join(__dirname, '..', 'node_modules')));

config.projects.map(p => (
(tokens[p.id] || (tokens[p.id] = {})),
Expand All @@ -82,9 +84,12 @@ app.use('/nm', express.static(path.join('node_modules')));
require('./services/ot')(server, p, tokens[p.id])
));
app.get('/', function(req, res) {
if ('logout' in req.query) {
return res.status(401).send('<meta http-equiv="refresh" content="0; url=/">');
}
if (req.isAuthenticated()) {
var list = config.projects
.filter(p => p.editors.indexOf(req.user.id) >= 0)
.filter(p => checkAccess(p, req.user.id))
return res.render('login.html', {projects: list});
}
res.send('no access')
Expand Down
23 changes: 15 additions & 8 deletions app/services/editor.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
const path = require('path');
const fs = require('fs');

const router = module.exports = require('express').Router();
const formidable = require('formidable');
const ncp = require('ncp').ncp;

const git = require('../git-tool');
const promisify = require('../helpers').promisify;
const ncp = require('ncp').ncp;
const mime = require('../mime');

const safePath = val => decodeURI(val).replace(/|\.\.|\/\//g, '');
const getRoot = req => Promise.resolve(req.project.path);
Expand Down Expand Up @@ -54,19 +57,23 @@ router.put('/copy/*', (req, res) => {
router.get('/tree', function(req, res) {
var dir = safePath(req.query.id);
dir = dir == '#' && '/' || dir;
excludes = ['.', '..'].concat(req.project.excludes || [ '.git', 'node_modules']);
excludes = ['.', '..', '.htdigest', 'projects.json'].concat(req.project.excludes || [ '.git', 'node_modules']);
const data = getRoot(req)
.then(root => promisify(fs.readdir)(path.join(root, dir))
.then(list => list.filter(name => name && excludes.indexOf(name) < 0))
.then(list => Promise.all(list.map(name => promisify(fs.stat)(path.join(root, dir, name))
.then(stats => ({
children: stats.isDirectory(),
type: stats.isDirectory() ? 'default' : "file",
.then(list => Promise.all(list.map(name =>
// promisify(fs.stat)(path.join(root, dir, name))
mime(path.join(root, dir, name))
.then(m => ({
children: m.stats.isDirectory(),
type: m.stats.isDirectory() ? 'default' : "file",
text: name,
id: path.join(dir, name),
mime: m.mime,
size: m.stats.size,
id: path.join(dir, name).replace(/\\/g, '/'),
// icon: stats.isDirectory() ? 'jstree-folder' : "jstree-file",
}))
.catch(e => 0)
.catch(e => (console.error(e),0))
).filter(i => i))
)
)
Expand Down
26 changes: 16 additions & 10 deletions app/services/ot.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const fs = require('fs');
const path = require('path');

const mmm = require('mmmagic');
const mime = require('../mime');
const chokidar = require('chokidar');
const sio = require('socket.io');
const ot = require('ot-jes');
Expand All @@ -12,8 +12,6 @@ const client = require('../ot-client');
const ns = {};
const getFile = (root, file) => promisify(fs.readFile)(path.join(root, file));
const setFile = (root, file, data) => promisify(fs.writeFile)(path.join(root, file), data);
const magic = new mmm.Magic(mmm.MAGIC_MIME_TYPE);
const getMime = (root, file) => promisify(magic.detectFile, magic)(path.join(root, file));

module.exports = (server, project, tokens) => {
ns[project.ws] = {};
Expand All @@ -30,28 +28,36 @@ module.exports = (server, project, tokens) => {
ignored: project.excludes || /node_modules/,
persistent: true,
})
.on('change', path => {
const docId = '/' + path;
.on('change', file => {
const docId = '/' + file;
mime(path.join(project.path, docId))
.then(m => canEdit(m.mime) &&
Promise.all([getFile(project.path, docId), getDoc(io, project, docId, 'host')])
.then(p => {
const ob = p[1], text = p[0];
client(io, ob, text);
})
)
})
}
function canEdit(mime) {
var m = mime.split('/');
return m[0] == 'text' || m[0] == 'application'
&& ['xml', 'sql', 'json', 'javascript', 'atom+xml', 'soap+xml', 'xhtml+xml', 'xml-dtd', 'xop+xml'].indexOf(m[1]) >=0
}

function getDoc(io, project, docId, name) {
const pp = ns[project.ws];
if (pp[docId])
return Promise.resolve(pp[docId])
else {
return Promise.all([getFile(project.path, docId), getMime(project.path, docId)])
return Promise.all([getFile(project.path, docId), mime(path.join(project.path, docId))])
.then(p => {
const text = p[0], type = p[1];
if (type.indexOf('audio') == 0 || type.indexOf('application') == 0)
throw new Error('uneditable format');
const text = p[0], m = p[1].mime;
if (!canEdit(m))
throw new Error('binary format');
const doc = new ot.EditorSocketIOServer(text.toString(), 0, docId);
const ob = pp[docId] = {ot: doc, type: type, id: docId};
const ob = pp[docId] = {ot: doc, type: m, id: docId};
io.of(docId)
.on('connection', function(socket) {
function clients(mode) {
Expand Down
108 changes: 65 additions & 43 deletions jesed
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const fs = require('fs');
const path = require('path');

const yargs = require('yargs');
const opn = require('opn');
const opn = require('open');
const prompt = require('prompt');

const {promisify, walk, getAllFiles} = require('./app/helpers');
Expand All @@ -14,24 +14,56 @@ const md5 = data => crypto.createHash('md5').update(data).digest("hex");

const config = {};

//create new project file
fs.copyFile(path.join(__dirname, 'projects.json.dist'), path.join(__dirname, 'projects.json'), fs.constants.COPYFILE_EXCL, err => 0);

const getPass = () =>
promisify(fs.readFile)('.htdigest')
.catch(e => '')
// .catch(e => '')
.then(text => text.toString().split(/\r\n?|\n/).filter(a => a).map(i => i.split(':')))

const getProjects = () =>
promisify(fs.readFile)('projects.json')
.catch(e => '[]')
const setProjects = (obj) => promisify(fs.writeFile)('projects.json', JSON.stringify(obj, null, 2)).then(a => obj);

const getProjects = (name) =>
promisify(fs.readFile)(name || 'projects.json')
.catch(e => promisify(fs.readFile)('projects.json.dist').then(setProjects))
.then(text => JSON.parse(text.toString()))


const setPass = (pass) =>
Promise.resolve(pass.map(i => i.join(':')).join('\n'))
.then(text => promisify(fs.writeFile)('.htdigest', text))

yargs
const promptPass = argv => new Promise((resolve, reject) => {
prompt.start();
const schema = {
properties: {
name: {
pattern: /^[a-zA-Z\s\-]+$/,
message: 'Name must be only letters, spaces, or dashes',
required: true,
},
password: {
hidden: true,
replace: '*',
}
}
};
argv && argv.name && delete schema.properties.name;
return promisify(prompt.get)(schema)
.then(result => {
const pwd = (a, pass) => { a[2] = pass; a[2] = md5(a.join(':')); return a; };
const name = argv && argv.name || result.name;
getPass().catch(e => [])
.then(p =>
p.filter(i => i[0] == name).length
&& p.map(i => i[0] == name && pwd(i, result.password) || i)
|| (p.push(pwd([name, 'Users', ''], result.password)), p)
)
.then(setPass)
.then(a => console.log('password set'))
.then(resolve)
});
})

const args = () => yargs
.command(['serve [mode]', 's'], 'start the server', (yargs) => {
yargs
.positional('mode', {
Expand All @@ -48,11 +80,11 @@ yargs
.command(['users'], 'list available users', {}, (argv) => {
getPass().then(i => i.map(a => console.log(a[0])))
})
.command(['user <mode> <name>'], 'set or remove allowed users', (yargs) => {
.command(['user [mode] [name]'], 'set or remove allowed users', (yargs) => {
yargs
.positional('mode', {
describe: 'set or remove',
// default: 'set'
default: 'set'
})
}, (argv) => {
if (argv.mode == 'remove') {
Expand All @@ -61,58 +93,48 @@ yargs
.then(setPass)
.then(a => console.log('password removed'));
}
prompt.start();
const schema = {
properties: {
name: {
pattern: /^[a-zA-Z\s\-]+$/,
message: 'Name must be only letters, spaces, or dashes',
required: true,
},
password: {
hidden: true,
replace: '*',
}
}
};
delete schema.properties.name; //TODO: make name not required and mode too
promisify(prompt.get)(schema)
.then(result => {
const pwd = (a, pass) => { a[2] = pass; a[2] = md5(a.join(':')); return a; };
getPass()
.then(p =>
p.filter(i => i[0] == argv.name).length
&& p.map(i => i[0] == argv.name && pwd(i, result.password) || i)
|| (p.push(pwd([argv.name, 'Users', ''], result.password)), p)
)
.then(setPass)
.then(a => console.log('password set'));
});
promptPass(argv);
})
.option('verbose', {
alias: 'v',
default: false
})
.command(['$0'], 'the serve command', () => {}, (argv) => {
serve('local');
})
.showHelpOnFail(true)
.demandCommand(1, '')
.argv

//create new project file
promisify(fs.stat)('projects.json')
.catch(e => getProjects(path.join(__dirname, 'projects.json.dist')).then(setProjects))
.then(args);

function serve(mode) {
Promise.all([
getPass(),
getProjects(),
require('./package.json'),
])
.catch(e => {
console.error('missing config', e)
process.exit();
})
.then(files => {
Object.assign(config, files[2].config[mode])
config.pass = files[0].reduce((p, i) => (p[i[0]] = i[2], p) , {});
config.projects = files[1];
server(config);
mode == 'local' && opn('http://localhost:' + config.port);
if (mode == 'local') {
console.log('open in browser url http://localhost:' + config.port);
try {
opn('http://localhost:' + config.port);
} catch(e) {};
}
})
.catch(e => {
// console.error('missing config', e)
console.error('type `jesed user add` to add user at next time')
promptPass()
.then(a => serve(mode))
.catch(e => process.exit());
})
}

Expand Down
Loading

0 comments on commit 4aa47ff

Please sign in to comment.