feature (Share): workable version for sharing

This commit is contained in:
Mickael KERJEAN
2018-10-04 17:03:17 +10:00
parent 2c086c24b7
commit 8f62551787
43 changed files with 1412 additions and 348 deletions

View File

@ -50,11 +50,15 @@
--secondary: #466372; --secondary: #466372;
--emphasis-secondary: #466372; --emphasis-secondary: #466372;
--light: #909090; --light: #909090;
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
--super-light: #f4f4f4; --super-light: #f4f4f4;
======= =======
--super-light: #F7F9FA; --super-light: #F7F9FA;
>>>>>>> 801aef8... improvement (incremental): update colors and improve page when current folder is empty >>>>>>> 801aef8... improvement (incremental): update colors and improve page when current folder is empty
=======
--super-light: #f9fafc;
>>>>>>> 384b3e0... feature (Share): workable version for sharing
--error: #f26d6d; --error: #f26d6d;
--success: #63d9b1; --success: #63d9b1;
--dark: #313538; --dark: #313538;

View File

@ -45,7 +45,7 @@ export class BreadCrumb extends React.Component {
} }
render(Element) { render(Element) {
if(location.search === "?nav=false") return null; if(new window.URL(location.href).searchParams.get("nav") === "false") return null;
const Path = Element? Element : PathElement; const Path = Element? Element : PathElement;
return ( return (
@ -136,7 +136,7 @@ export class PathElementWrapper extends React.Component {
return ( return (
<li className={className}> <li className={className}>
<NgIf cond={this.props.isLast === false}> <NgIf cond={this.props.isLast === false}>
<Link to={"/files" + this.props.path.full || "/"} className="label"> <Link to={"/files" + (this.props.path.full || "/") + location.search} className="label">
<NgIf cond={this.props.path.minify !== true}> <NgIf cond={this.props.path.minify !== true}>
{this.limitSize(this.props.path.label)} {this.limitSize(this.props.path.label)}
</NgIf> </NgIf>

View File

@ -23,4 +23,6 @@ button{
background: var(--emphasis); background: var(--emphasis);
color: white color: white
} }
&.transparent{
}
} }

View File

@ -9,8 +9,10 @@ export class Container extends React.Component {
} }
render() { render() {
const style = this.props.maxWidth ? {maxWidth: this.props.maxWidth} : {}; const style = this.props.maxWidth ? {maxWidth: this.props.maxWidth} : {};
let className = "component_container";
if(this.props.className) className += " "+this.props.className;
return ( return (
<div className="component_container" style={style}> <div className={className} style={style}>
{this.props.children} {this.props.children}
</div> </div>
); );

View File

@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { Session } from '../model/'; import { Session } from '../model/';
import { Container, Loader, Icon } from '../components/'; import { Container, Loader, Icon } from '../components/';
import { memory } from '../helpers/'; import { memory, currentShare } from '../helpers/';
import '../pages/error.scss'; import '../pages/error.scss';
@ -19,7 +19,7 @@ export function LoggedInOnly(WrappedComponent){
} }
componentDidMount(){ componentDidMount(){
if(this.state.is_logged_in === false){ if(this.state.is_logged_in === false && currentShare() === null){
Session.currentUser().then((res) => { Session.currentUser().then((res) => {
if(res.is_authenticated === false){ if(res.is_authenticated === false){
this.props.error({message: "Authentication Required"}); this.props.error({message: "Authentication Required"});
@ -38,7 +38,7 @@ export function LoggedInOnly(WrappedComponent){
} }
render(){ render(){
if(this.state.is_logged_in === true){ if(this.state.is_logged_in === true || currentShare() !== null){
return <WrappedComponent {...this.props} />; return <WrappedComponent {...this.props} />;
} }
return null; return null;
@ -65,7 +65,7 @@ export function ErrorPage(WrappedComponent){
const message = this.state.error.message || "There is nothing in here"; const message = this.state.error.message || "There is nothing in here";
return ( return (
<div> <div>
<Link to="/" className="backnav"> <Link to={`/${window.location.search}`} className="backnav">
<Icon name="arrow_left" />home <Icon name="arrow_left" />home
</Link> </Link>
<Container> <Container>

View File

@ -7,7 +7,7 @@ function Data(){
this._init(); this._init();
} }
const DB_VERSION = 2; const DB_VERSION = 3;
Data.prototype._init = function(){ Data.prototype._init = function(){
const request = indexedDB.open('nuage', DB_VERSION); const request = indexedDB.open('nuage', DB_VERSION);
@ -27,30 +27,34 @@ Data.prototype._setup = function(e){
let store; let store;
let db = e.target.result; let db = e.target.result;
if(e.oldVersion == 1){ if(e.oldVersion == 1) {
// we've change the schema on v2 adding an index, let's flush // we've change the schema on v2 adding an index, let's flush
// to make sure everything will be fine // to make sure everything will be fine
db.deleteObjectStore(this.FILE_PATH); db.deleteObjectStore(this.FILE_PATH);
db.deleteObjectStore(this.FILE_CONTENT); db.deleteObjectStore(this.FILE_CONTENT);
}else if(e.oldVersion == 2){
// we've change the primary key to be a (path,share)
db.deleteObjectStore(this.FILE_PATH);
db.deleteObjectStore(this.FILE_CONTENT);
} }
store = db.createObjectStore(this.FILE_PATH, {keyPath: "path"}); store = db.createObjectStore(this.FILE_PATH, {keyPath: ["share", "path"]});
store.createIndex("idx_path", "path", { unique: true }); store.createIndex("idx_path", ["share", "path"], { unique: true });
store = db.createObjectStore(this.FILE_CONTENT, {keyPath: "path"}); store = db.createObjectStore(this.FILE_CONTENT, {keyPath: ["share", "path"]});
store.createIndex("idx_path", "path", { unique: true }); store.createIndex("idx_path", ["share", "path"], { unique: true });
} }
/* /*
* Fetch a record using its path, can be whether a file path or content * Fetch a record using its path, can be whether a file path or content
*/ */
Data.prototype.get = function(type, path){ Data.prototype.get = function(type, key){
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({}); if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
return this.db.then((db) => { return this.db.then((db) => {
const tx = db.transaction(type, "readonly"); const tx = db.transaction(type, "readonly");
const store = tx.objectStore(type); const store = tx.objectStore(type);
const query = store.get(path); const query = store.get(key);
return new Promise((done, error) => { return new Promise((done, error) => {
query.onsuccess = (e) => { query.onsuccess = (e) => {
let data = query.result; let data = query.result;
@ -61,13 +65,13 @@ Data.prototype.get = function(type, path){
}).catch(() => Promise.resolve(null)); }).catch(() => Promise.resolve(null));
} }
Data.prototype.update = function(type, path, fn, exact = true){ Data.prototype.update = function(type, key, fn, exact = true){
return this.db.then((db) => { return this.db.then((db) => {
const tx = db.transaction(type, "readwrite"); const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type); const store = tx.objectStore(type);
const range = exact === true? IDBKeyRange.only(path) : IDBKeyRange.bound( const range = exact === true? IDBKeyRange.only(key) : IDBKeyRange.bound(
path, [key[0], key[1]],
path+'\uFFFF', [key[0], key[1]+'\uFFFF'],
false, true false, true
); );
const request = store.openCursor(range); const request = store.openCursor(range);
@ -77,7 +81,7 @@ Data.prototype.update = function(type, path, fn, exact = true){
const cursor = event.target.result; const cursor = event.target.result;
if(!cursor) return done(new_data); if(!cursor) return done(new_data);
new_data = fn(cursor.value || null); new_data = fn(cursor.value || null);
cursor.delete(cursor.value.path); cursor.delete([key[0], cursor.value.path]);
store.put(new_data); store.put(new_data);
cursor.continue(); cursor.continue();
}; };
@ -86,11 +90,11 @@ Data.prototype.update = function(type, path, fn, exact = true){
} }
Data.prototype.upsert = function(type, path, fn){ Data.prototype.upsert = function(type, key, fn){
return this.db.then((db) => { return this.db.then((db) => {
const tx = db.transaction(type, "readwrite"); const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type); const store = tx.objectStore(type);
const query = store.get(path); const query = store.get(key);
return new Promise((done, error) => { return new Promise((done, error) => {
query.onsuccess = (e) => { query.onsuccess = (e) => {
const new_data = fn(query.result || null); const new_data = fn(query.result || null);
@ -105,7 +109,7 @@ Data.prototype.upsert = function(type, path, fn){
}).catch(() => Promise.resolve(null)); }).catch(() => Promise.resolve(null));
} }
Data.prototype.add = function(type, path, data){ Data.prototype.add = function(type, key, data){
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({}); if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
return this.db.then((db) => { return this.db.then((db) => {
@ -119,28 +123,28 @@ Data.prototype.add = function(type, path, data){
}).catch(() => Promise.resolve(null)); }).catch(() => Promise.resolve(null));
} }
Data.prototype.remove = function(type, path, exact = true){ Data.prototype.remove = function(type, key, exact = true){
return this.db.then((db) => { return this.db.then((db) => {
const tx = db.transaction(type, "readwrite"); const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type); const store = tx.objectStore(type);
if(exact === true){ if(exact === true){
const req = store.delete(path); const req = store.delete(key);
return new Promise((done, err) => { return new Promise((done, err) => {
req.onsuccess = () => done(); req.onsuccess = () => done();
req.onerror = err; req.onerror = err;
}); });
}else{ }else{
const request = store.openCursor(IDBKeyRange.bound( const request = store.openCursor(IDBKeyRange.bound(
path, [key[0], key[1]],
path+'\uFFFF', [key[0], key[1]+'\uFFFF'],
true, true true, true
)); ));
return new Promise((done, err) => { return new Promise((done, err) => {
request.onsuccess = function(event) { request.onsuccess = function(event) {
const cursor = event.target.result; const cursor = event.target.result;
if(cursor){ if(cursor){
cursor.delete(cursor.value.path); cursor.delete([key[0], cursor.value.path]);
cursor.continue(); cursor.continue();
}else{ }else{
done(); done();
@ -151,12 +155,15 @@ Data.prototype.remove = function(type, path, exact = true){
}).catch(() => Promise.resolve(null)); }).catch(() => Promise.resolve(null));
} }
Data.prototype.fetchAll = function(fn, type = this.FILE_PATH, key = "/"){ Data.prototype.fetchAll = function(fn, type = this.FILE_PATH, key){
return this.db.then((db) => { return this.db.then((db) => {
const tx = db.transaction([type], "readonly"); const tx = db.transaction([type], "readonly");
const store = tx.objectStore(type); const store = tx.objectStore(type);
const index = store.index("idx_path"); const index = store.index("idx_path");
const request = index.openCursor(IDBKeyRange.bound(key, key+("z".repeat(5000)))); const request = index.openCursor(IDBKeyRange.bound(
[key[0], key[1]],
[key[0], key[1]+("z".repeat(5000))]
));
return new Promise((done, error) => { return new Promise((done, error) => {
request.onsuccess = function(event) { request.onsuccess = function(event) {

View File

@ -2,3 +2,16 @@ export function leftPad(str, length, pad = "0"){
if(typeof str !== 'string' || typeof pad !== 'string' || str.length >= length || !pad.length > 0) return str; if(typeof str !== 'string' || typeof pad !== 'string' || str.length >= length || !pad.length > 0) return str;
return leftPad(pad + str, length, pad); return leftPad(pad + str, length, pad);
} }
export function copyToClipboard (str){
if(!str) return
let $input = document.createElement("input");
$input.setAttribute("type", "text");
$input.setAttribute("style", "position: absolute; top:0;left:0;background:red")
$input.setAttribute("display", "none");
document.body.appendChild($input);
$input.value = str;
$input.select();
document.execCommand("copy");
$input.remove();
}

View File

@ -4,13 +4,13 @@ export { debounce, throttle } from './backpressure';
export { encrypt, decrypt } from './crypto'; export { encrypt, decrypt } from './crypto';
export { event } from './events'; export { event } from './events';
export { cache } from './cache'; export { cache } from './cache';
export { pathBuilder, basename, dirname, absoluteToRelative, filetype } from './path'; export { pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare, appendShareToUrl } from './path';
export { memory } from './memory'; export { memory } from './memory';
export { prepare } from './navigate'; export { prepare } from './navigate';
export { invalidate, http_get, http_post, http_delete } from './ajax'; export { invalidate, http_get, http_post, http_delete } from './ajax';
export { prompt, alert, confirm } from './popup'; export { prompt, alert, confirm } from './popup';
export { notify } from './notify'; export { notify } from './notify';
export { gid, randomString } from './random'; export { gid, randomString } from './random';
export { leftPad } from './common'; export { leftPad, copyToClipboard } from './common';
export { getMimeType } from './mimetype'; export { getMimeType } from './mimetype';
export { settings_get, settings_put } from './settings'; export { settings_get, settings_put } from './settings';

View File

@ -35,3 +35,19 @@ export function absoluteToRelative(from, to){
} }
return r; return r;
} }
export function currentShare(){
return new window.URL(location.href).searchParams.get("share") || ""
}
export function appendShareToUrl(link) {
let url = new window.URL(location.href);
let share = url.searchParams.get("share");
if(share){
url = new window.URL(location.origin + link)
url.searchParams.set("share", share)
return url.pathname + url.search
}
return link;
}

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
import { http_get, http_post, prepare, basename, dirname, pathBuilder } from '../helpers/'; import { http_get, http_post, prepare, basename, dirname, pathBuilder } from '../helpers/';
import { filetype } from '../helpers/'; import { filetype, currentShare, appendShareToUrl } from '../helpers/';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { cache } from '../helpers/'; import { cache } from '../helpers/';
@ -46,16 +46,17 @@ class FileSystem{
} }
_ls_from_http(path){ _ls_from_http(path){
const url = '/api/files/ls?path='+prepare(path); let url = appendShareToUrl('/api/files/ls?path='+prepare(path));
return http_get(url).then((response) => { return http_get(url).then((response) => {
return cache.upsert(cache.FILE_PATH, path, (_files) => { return cache.upsert(cache.FILE_PATH, [currentShare(), path], (_files) => {
let store = Object.assign({ let store = Object.assign({
share: currentShare(),
path: path, path: path,
results: null, results: null,
access_count: 0, access_count: 0,
metadata: null metadata: null
}, _files); }, _files);
store.access_count += 1;
store.results = response.results || []; store.results = response.results || [];
store.results = store.results.map((f) => { store.results = store.results.map((f) => {
f.path = pathBuilder(path, f.name, f.type); f.path = pathBuilder(path, f.name, f.type);
@ -64,6 +65,8 @@ class FileSystem{
store.metadata = response.metadata; store.metadata = response.metadata;
if(_files && _files.results){ if(_files && _files.results){
store.access_count = _files.access_count;
// find out which entry we want to keep from the cache // find out which entry we want to keep from the cache
let _files_virtual_to_keep = _files.results.filter((file) => { let _files_virtual_to_keep = _files.results.filter((file) => {
return file.icon === 'loading'; return file.icon === 'loading';
@ -101,44 +104,47 @@ class FileSystem{
} }
_ls_from_cache(path, _record_access = false){ _ls_from_cache(path, _record_access = false){
if(_record_access === false){ return cache.get(cache.FILE_PATH, [currentShare(), path]).then((response) => {
return cache.get(cache.FILE_PATH, path).then((response) => { if(!response || !response.results) return null;
if(!response || !response.results) return null; if(this.current_path === path){
if(this.current_path === path){ this.obs && this.obs.next({
this.obs && this.obs.next({ status: 'ok',
status: 'ok', results: response.results,
results: response.results, metadata: response.metadata
metadata: response.metadata });
}
return response;
}).then((e) => {
requestAnimationFrame(() => {
if(_record_access === true){
cache.upsert(cache.FILE_PATH, [currentShare(), path], (response) => {
if(!response || !response.results) return null;
if(this.current_path === path){
this.obs && this.obs.next({
status: 'ok',
results: response.results,
metadata: response.metadata
});
}
response.last_access = new Date();
response.access_count += 1;
return response;
}); });
} }
return response;
}); });
}else{ return Promise.resolve(e);
return cache.upsert(cache.FILE_PATH, path, (response) => { });
if(!response || !response.results) return null;
if(this.current_path === path){
this.obs && this.obs.next({
status: 'ok',
results: response.results,
metadata: response.metadata
});
}
response.last_access = new Date();
response.access_count += 1;
return response;
});
}
} }
rm(path){ rm(path){
const url = '/api/files/rm?path='+prepare(path); const url = appendShareToUrl('/api/files/rm?path='+prepare(path));
return this._replace(path, 'loading') return this._replace(path, 'loading')
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res)) .then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
.then(() => http_get(url)) .then(() => http_get(url))
.then((res) => { .then((res) => {
return cache.remove(cache.FILE_CONTENT, path) return cache.remove(cache.FILE_CONTENT, [currentShare(), path])
.then(cache.remove(cache.FILE_CONTENT, path, false)) .then(cache.remove(cache.FILE_CONTENT, [currentShare(), path], false))
.then(cache.remove(cache.FILE_PATH, dirname(path), false)) .then(cache.remove(cache.FILE_PATH, [currentShare(), dirname(path)], false))
.then(this._remove(path, 'loading')) .then(this._remove(path, 'loading'))
.then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res)) .then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res))
}) })
@ -150,14 +156,15 @@ class FileSystem{
} }
cat(path){ cat(path){
const url = '/api/files/cat?path='+prepare(path); const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
return http_get(url, 'raw') return http_get(url, 'raw')
.then((res) => { .then((res) => {
if(this.is_binary(res) === true){ if(this.is_binary(res) === true){
return Promise.reject({code: 'BINARY_FILE'}); return Promise.reject({code: 'BINARY_FILE'});
} }
return cache.upsert(cache.FILE_CONTENT, path, (response) => { return cache.upsert(cache.FILE_CONTENT, [currentShare(), path], (response) => {
let file = response? response : { let file = response? response : {
share: currentShare(),
path: path, path: path,
last_update: null, last_update: null,
last_access: null, last_access: null,
@ -173,7 +180,7 @@ class FileSystem{
.catch((err) => { .catch((err) => {
if(err.code === 'BINARY_FILE') return Promise.reject(err); if(err.code === 'BINARY_FILE') return Promise.reject(err);
return cache.update(cache.FILE_CONTENT, path, (response) => { return cache.update(cache.FILE_CONTENT, [currentShare(), path], (response) => {
response.last_access = new Date(); response.last_access = new Date();
response.access_count += 1; response.access_count += 1;
return response; return response;
@ -184,12 +191,12 @@ class FileSystem{
}); });
} }
url(path){ url(path){
const url = '/api/files/cat?path='+prepare(path); const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
return Promise.resolve(url); return Promise.resolve(url);
} }
save(path, file){ save(path, file){
const url = '/api/files/cat?path='+prepare(path); const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
let formData = new window.FormData(); let formData = new window.FormData();
formData.append('file', file, "test"); formData.append('file', file, "test");
return this._replace(path, 'loading') return this._replace(path, 'loading')
@ -207,7 +214,7 @@ class FileSystem{
} }
mkdir(path, step){ mkdir(path, step){
const url = '/api/files/mkdir?path='+prepare(path), const url = appendShareToUrl('/api/files/mkdir?path='+prepare(path)),
origin_path = pathBuilder(this.current_path, basename(path), 'directoy'), origin_path = pathBuilder(this.current_path, basename(path), 'directoy'),
destination_path = path; destination_path = path;
@ -240,8 +247,9 @@ class FileSystem{
.then(() => { .then(() => {
return this._replace(destination_path, null, 'loading') return this._replace(destination_path, null, 'loading')
.then(() => origin_path !== destination_path ? this._remove(origin_path, 'loading') : Promise.resolve()) .then(() => origin_path !== destination_path ? this._remove(origin_path, 'loading') : Promise.resolve())
.then(() => cache.add(cache.FILE_PATH, destination_path, { .then(() => cache.add(cache.FILE_PATH, [currentShare(), destination_path], {
path: destination_path, path: destination_path,
share: currentShare(),
results: [], results: [],
access_count: 0, access_count: 0,
last_access: null, last_access: null,
@ -310,12 +318,12 @@ class FileSystem{
function query(){ function query(){
if(file){ if(file){
const url = '/api/files/cat?path='+prepare(path); const url = appendShareToUrl('/api/files/cat?path='+prepare(path));
let formData = new window.FormData(); let formData = new window.FormData();
formData.append('file', file); formData.append('file', file);
return http_post(url, formData, 'multipart'); return http_post(url, formData, 'multipart');
}else{ }else{
const url = '/api/files/touch?path='+prepare(path); const url = appendShareToUrl('/api/files/touch?path='+prepare(path));
return http_get(url); return http_get(url);
} }
} }
@ -331,7 +339,7 @@ class FileSystem{
} }
mv(from, to){ mv(from, to){
const url = '/api/files/mv?from='+prepare(from)+"&to="+prepare(to), const url = appendShareToUrl('/api/files/mv?from='+prepare(from)+"&to="+prepare(to)),
origin_path = from, origin_path = from,
destination_path = to; destination_path = to;
@ -344,11 +352,11 @@ class FileSystem{
.then(() => this._replace(destination_path, null, 'loading')) .then(() => this._replace(destination_path, null, 'loading'))
.then(() => this._refresh(origin_path, destination_path)) .then(() => this._refresh(origin_path, destination_path))
.then(() => { .then(() => {
cache.update(cache.FILE_PATH, origin_path, (data) => { cache.update(cache.FILE_PATH, [currentShare(), origin_path], (data) => {
data.path = data.path.replace(origin_path, destination_path); data.path = data.path.replace(origin_path, destination_path);
return data; return data;
}, false); }, false);
cache.update(cache.FILE_CONTENT, origin_path, (data) => { cache.update(cache.FILE_CONTENT, [currentShare(), origin_path], (data) => {
data.path = data.path.replace(origin_path, destination_path); data.path = data.path.replace(origin_path, destination_path);
return data; return data;
}, false); }, false);
@ -369,7 +377,7 @@ class FileSystem{
if(value.access_count >= 1 && value.path !== "/"){ if(value.access_count >= 1 && value.path !== "/"){
data.push(value); data.push(value);
} }
}).then(() => { }, cache.FILE_PATH, [currentShare(), "/"]).then(() => {
return Promise.resolve( return Promise.resolve(
data data
.sort((a,b) => a.access_count > b.access_count? -1 : 1) .sort((a,b) => a.access_count > b.access_count? -1 : 1)
@ -389,8 +397,9 @@ class FileSystem{
}); });
function update_cache(result){ function update_cache(result){
return cache.upsert(cache.FILE_CONTENT, path, (response) => { return cache.upsert(cache.FILE_CONTENT, [currentShare(), path], (response) => {
if(!response) response = { if(!response) response = {
share: currentShare(),
path: path, path: path,
last_access: null, last_access: null,
last_update: null, last_update: null,
@ -413,7 +422,7 @@ class FileSystem{
} }
_replace(path, icon, icon_previous){ _replace(path, icon, icon_previous){
return cache.update(cache.FILE_PATH, dirname(path), function(res){ return cache.update(cache.FILE_PATH, [currentShare(), dirname(path)], function(res){
res.results = res.results.map((file) => { res.results = res.results.map((file) => {
if(file.name === basename(path) && file.icon == icon_previous){ if(file.name === basename(path) && file.icon == icon_previous){
if(!icon){ delete file.icon; } if(!icon){ delete file.icon; }
@ -425,7 +434,7 @@ class FileSystem{
}); });
} }
_add(path, icon){ _add(path, icon){
return cache.upsert(cache.FILE_PATH, dirname(path), function(res){ return cache.upsert(cache.FILE_PATH, [currentShare(), dirname(path)], function(res){
if(!res || !res.results){ if(!res || !res.results){
res = { res = {
path: dirname(path), path: dirname(path),
@ -445,7 +454,7 @@ class FileSystem{
}); });
} }
_remove(path, previous_icon){ _remove(path, previous_icon){
return cache.update(cache.FILE_PATH, dirname(path), function(res){ return cache.update(cache.FILE_PATH, [currentShare(), dirname(path)], function(res){
if(!res) return null; if(!res) return null;
res.results = res.results.filter((file) => { res.results = res.results.filter((file) => {
return file.name === basename(path) && file.icon == previous_icon ? false : true; return file.name === basename(path) && file.icon == previous_icon ? false : true;
@ -461,5 +470,4 @@ class FileSystem{
} }
} }
export const Files = new FileSystem(); export const Files = new FileSystem();

View File

@ -1,4 +1,4 @@
import { http_get, http_post, http_delete } from '../helpers/'; import { http_get, http_post, http_delete, appendShareToUrl } from '../helpers/';
class ShareModel { class ShareModel {
constructor(){} constructor(){}
@ -25,19 +25,20 @@ class ShareModel {
} }
upsert(obj){ upsert(obj){
const url = `/api/share/${obj.id}` const url = appendShareToUrl(`/api/share/${obj.id}`)
const data = Object.assign({}, obj); const data = Object.assign({}, obj);
delete data.role; delete data.role;
return http_post(url, data); return http_post(url, data);
} }
remove(id){ remove(id){
const url = `/api/share/${id}`; const url = appendShareToUrl(`/api/share/${id}`);
return http_delete(url); return http_delete(url);
} }
proof(id, data){ proof(id, data){
// TODO const url = `/api/share/${id}/proof`;
return http_post(url, data).then((res) => res.result);
} }
} }

View File

@ -5,15 +5,17 @@
flex-direction: column; flex-direction: column;
h1{margin: 5px 0; font-size: 3.1em;} h1{margin: 5px 0; font-size: 3.1em;}
h2{margin: 10px 0; font-weight: normal; opacity: 0.9;} h2{margin: 10px 0; font-weight: normal; opacity: 0.9; font-weight: 100;}
p{font-style: italic;} p{font-style: italic;}
a{border-bottom: 1px dashed;} a{border-bottom: 1px dashed;}
} }
.backnav { .backnav {
font-weight: 100;
.component_icon { .component_icon {
height: 25px; height: 23px;
margin-right: -2px; margin-right: -3px;
vertical-align: middle;
} }
line-height: 25px; line-height: 25px;
} }

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Files } from '../model/'; import { Files } from '../model/';
import { notify, alert } from '../helpers/'; import { notify, alert, currentShare } from '../helpers/';
import Path from 'path'; import Path from 'path';
import Worker from "../worker/search.worker.js"; import Worker from "../worker/search.worker.js";
import { Observable } from "rxjs/Observable"; import { Observable } from "rxjs/Observable";
@ -371,11 +371,12 @@ export const onUpload = function(path, e){
const worker = new Worker();9 const worker = new Worker();
export const onSearch = (keyword, path = "/") => { export const onSearch = (keyword, path = "/") => {
worker.postMessage({ worker.postMessage({
action: "search::find", action: "search::find",
path: path, path: path,
share: currentShare(),
keyword: keyword keyword: keyword
}); });
return new Observable((obs) => { return new Observable((obs) => {

View File

@ -244,6 +244,9 @@ export class FilesPage extends React.Component {
<Loader/> <Loader/>
</NgIf> </NgIf>
</div> </div>
<div className="sidebar close">
THIS IS A MENUBAR
</div>
</div> </div>
</div> </div>
); );

View File

@ -25,6 +25,13 @@
} }
} }
} }
.sidebar{
width: 250px;
transition: width 0.3s ease;
background: var(--light);
&.close{width: 0;}
}
} }
.scroll-y{ .scroll-y{

View File

@ -17,11 +17,12 @@ export class FrequentlyAccess extends React.Component {
return ( return (
<ReactCSSTransitionGroup transitionName="frequent-access" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={300}> <ReactCSSTransitionGroup transitionName="frequent-access" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={300}>
<Container> <Container>
<span>Quick Access</span>
<div className="component_frequently-access"> <div className="component_frequently-access">
{ {
this.props.files.map(function(path, index){ this.props.files.map(function(path, index){
return ( return (
<Link key={path} to={"/files"+path}> <Link key={path} to={"/files"+path+window.location.search}>
<Icon name={'directory'} /> <Icon name={'directory'} />
<div>{Path.basename(path)}</div> <div>{Path.basename(path)}</div>
</Link> </Link>

View File

@ -2,7 +2,7 @@
display: flex; display: flex;
a{ a{
width: 33.33%; width: 33.33%;
background: var(--bg-color); background: var(--light-color);
box-shadow: rgba(158, 163, 172, 0.3) 0px 19px 60px, rgba(158, 163, 172, 0.22) 0px 15px 20px; box-shadow: rgba(158, 163, 172, 0.3) 0px 19px 60px, rgba(158, 163, 172, 0.22) 0px 15px 20px;
overflow: hidden; overflow: hidden;
margin-right: 5px; margin-right: 5px;

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { NgIf, Icon } from '../../components/'; import { NgIf, Icon } from '../../components/';
import { Share } from '../../model/'; import { Share } from '../../model/';
import { randomString, notify, absoluteToRelative } from '../../helpers/'; import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/';
import './share.scss'; import './share.scss';
export class ShareComponent extends React.Component { export class ShareComponent extends React.Component {
@ -32,14 +32,20 @@ export class ShareComponent extends React.Component {
} }
componentDidMount(){ componentDidMount(){
Share.all(this.props.path) Share.all(this.props.path).then((existings) => {
.then((existings) => { this.refreshModal();
this.refreshModal(); this.setState({
this.setState({existings: existings}); existings: existings.sort((a, b) => {
return a.path.split("/").length > b.path.split("/").length;
})
}); });
});
} }
updateState(key, value){ updateState(key, value){
if(key === "role"){
this.setState(this.resetState());
}
if(this.state[key] === value){ if(this.state[key] === value){
this.setState({[key]: null}); this.setState({[key]: null});
}else{ }else{
@ -85,10 +91,13 @@ export class ShareComponent extends React.Component {
}); });
} }
onRegisterLink(e){ copyLinkInClipboard(link){
this.refs.$input.select(); copyToClipboard(link);
document.execCommand("copy");
notify.send("The link was copied in the clipboard", "INFO"); notify.send("The link was copied in the clipboard", "INFO");
}
onRegisterLink(e){
this.copyLinkInClipboard(this.refs.$input.value);
const link = { const link = {
role: this.state.role, role: this.state.role,
@ -137,9 +146,9 @@ export class ShareComponent extends React.Component {
return Share.upsert(link) return Share.upsert(link)
.then(() => { .then(() => {
if(this.state.url !== null && this.state.url !== this.state.id){ if(this.state.url !== null && this.state.url !== this.state.id){
this.onDeleteLink(this.state.id) this.onDeleteLink(this.state.id);
} }
return Promise.resolve() return Promise.resolve();
}) })
.then(() => this.setState(this.resetState())) .then(() => this.setState(this.resetState()))
.catch((err) => { .catch((err) => {
@ -152,11 +161,13 @@ export class ShareComponent extends React.Component {
render(){ render(){
const beautifulPath = function(from, to){ const beautifulPath = function(from, to){
return to; if(filetype(from) === "directory"){
const p = absoluteToRelative(from, to); from = from.split("/");
if(p === "./"){ from = from.slice(0, from.length - 1);
return "Current folder"; from = from.join("/");
} }
let p = absoluteToRelative(from, to);
return p.length < to.length ? p : to; return p.length < to.length ? p : to;
}; };
const urlify = function(str){ const urlify = function(str){
@ -197,8 +208,10 @@ export class ShareComponent extends React.Component {
this.state.existings && this.state.existings.map((link, i) => { this.state.existings && this.state.existings.map((link, i) => {
return ( return (
<div className="link-details" key={i}> <div className="link-details" key={i}>
<span className="role">{link.role}</span> <span onClick={this.copyLinkInClipboard.bind(this, window.location.origin+"/s/"+link.id)} className="copy role">
<span className="path">{beautifulPath(this.props.path, link.path)}</span> {link.role}
</span>
<span onClick={this.copyLinkInClipboard.bind(this, window.location.origin+"/s/"+link.id)} className="copy path">{beautifulPath(this.props.path, link.path)}</span>
<Icon onClick={this.onDeleteLink.bind(this, link.id)} name="delete"/> <Icon onClick={this.onDeleteLink.bind(this, link.id)} name="delete"/>
<Icon onClick={this.onLoad.bind(this, link)} name="edit"/> <Icon onClick={this.onLoad.bind(this, link)} name="edit"/>
</div> </div>
@ -221,16 +234,18 @@ export class ShareComponent extends React.Component {
<NgIf type="inline" cond={!this.state.show_advanced}><Icon name="arrow_bottom"/></NgIf> <NgIf type="inline" cond={!this.state.show_advanced}><Icon name="arrow_bottom"/></NgIf>
</h2> </h2>
<div className="share--content advanced-settings no-select"> <div className="share--content advanced-settings no-select">
<NgIf cond={this.state.show_advanced === true}> <NgIf cond={false}>
<SuperCheckbox value={this.state.can_manage_own} label="Can Manage Own" onChange={this.updateState.bind(this, 'can_manage_own')}/> <SuperCheckbox value={this.state.can_manage_own} label="Can Manage Own" onChange={this.updateState.bind(this, 'can_manage_own')}/>
<SuperCheckbox value={this.state.can_share} label="Can Share" onChange={this.updateState.bind(this, 'can_share')}/> <SuperCheckbox value={this.state.can_share} label="Can Share" onChange={this.updateState.bind(this, 'can_share')}/>
</NgIf>
<NgIf cond={this.state.show_advanced === true}>
<SuperCheckbox value={datify(this.state.expire)} label="Expiration" placeholder="The link won't be valid after" onChange={this.updateState.bind(this, 'expire')} inputType="date"/> <SuperCheckbox value={datify(this.state.expire)} label="Expiration" placeholder="The link won't be valid after" onChange={this.updateState.bind(this, 'expire')} inputType="date"/>
<SuperCheckbox value={this.state.url} label="Custom Link url" placeholder="beautiful_url" onChange={(val) => this.updateState('url', urlify(val))} inputType="text"/> <SuperCheckbox value={this.state.url} label="Custom Link url" placeholder="beautiful_url" onChange={(val) => this.updateState('url', urlify(val))} inputType="text"/>
</NgIf> </NgIf>
</div> </div>
<div className="shared-link"> <div className="shared-link">
<input ref="$input" onClick={this.onRegisterLink.bind(this)} type="text" value={window.location.origin+"/s/"+(this.state.url || this.state.id)} onChange={() => {}}/> <input ref="$input" className="copy" onClick={this.onRegisterLink.bind(this)} type="text" value={window.location.origin+"/s/"+(this.state.url || this.state.id)} onChange={() => {}}/>
</div> </div>
</NgIf> </NgIf>
</div> </div>
@ -240,15 +255,18 @@ export class ShareComponent extends React.Component {
const SuperCheckbox = (props) => { const SuperCheckbox = (props) => {
const onCheckboxTick = (e) => { const onCheckboxTick = (e) => {
if(props.inputType === undefined){
return props.onChange(e.target.checked ? true : false);
}
return props.onChange(e.target.checked ? "" : null); return props.onChange(e.target.checked ? "" : null);
}; };
const onValueChange = (e) => { const onValueChange = (e) => {
props.onChange(e.target.value); props.onChange(e.target.value);
}; };
const _is_expended = function(val){ const _is_expended = function(val){
return val === null || val === undefined ? false : true; return val === null || val === undefined || val === false ? false : true;
}(props.value); }(props.value);
return ( return (
<div className="component_supercheckbox"> <div className="component_supercheckbox">
<label> <label>

View File

@ -9,6 +9,9 @@
float: right; float: right;
} }
} }
.copy {
cursor: copy;
}
.share--content{ .share--content{
margin-bottom: 10px; margin-bottom: 10px;
@ -53,7 +56,7 @@
.path{ .path{
vertical-align: middle; vertical-align: middle;
font-size: 0.9em; font-size: 0.9em;
max-width: 180px; max-width: 170px;
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
overflow-x: hidden; overflow-x: hidden;

View File

@ -5,6 +5,8 @@ import { Card, NgIf, Icon, EventEmitter, Dropdown, DropdownButton, DropdownList,
import { pathBuilder, debounce } from '../../helpers/'; import { pathBuilder, debounce } from '../../helpers/';
import "./submenu.scss"; import "./submenu.scss";
let SEARCH_KEYWORD = "";
@EventEmitter @EventEmitter
export class Submenu extends React.Component { export class Submenu extends React.Component {
constructor(props){ constructor(props){
@ -14,6 +16,12 @@ export class Submenu extends React.Component {
search_input_visible: false, search_input_visible: false,
search_keyword: "" search_keyword: ""
}; };
if(SEARCH_KEYWORD){
this.state.search_input_visible = true;
this.state.search_keyword = SEARCH_KEYWORD;
}
this.onSearchChange_Backpressure = debounce(this.onSearchChange, 400); this.onSearchChange_Backpressure = debounce(this.onSearchChange, 400);
this._onEscapeKeyPress = (e) => { this._onEscapeKeyPress = (e) => {
if(e.keyCode === 27){ // escape key if(e.keyCode === 27){ // escape key
@ -38,8 +46,12 @@ export class Submenu extends React.Component {
componentDidMount(){ componentDidMount(){
window.addEventListener('keydown', this._onEscapeKeyPress); window.addEventListener('keydown', this._onEscapeKeyPress);
if(this.state.search_input_visible === true){
this.onSearchChange(this.state.search_keyword);
}
} }
componentWillUnmount(){ componentWillUnmount(){
SEARCH_KEYWORD = this.state.search_keyword;
window.removeEventListener('keydown', this._onEscapeKeyPress); window.removeEventListener('keydown', this._onEscapeKeyPress);
} }

View File

@ -214,12 +214,12 @@ export class ExistingThing extends React.Component {
return connectDragSource(connectDropNativeFile(connectDropFile( return connectDragSource(connectDropNativeFile(connectDropFile(
<div className={"component_thing view-"+this.props.view}> <div className={"component_thing view-"+this.props.view}>
<Link to={this.props.file.link + location.search || "#"}> <Link to={this.props.file.link + location.search}>
<Card ref="$card"className={this.state.hover} className={className}> <Card ref="$card"className={this.state.hover} className={className}>
<Image preview={this.state.preview} icon={this.props.file.icon || this.props.file.type} view={this.props.view} path={path.join(this.props.path, this.props.file.name)} /> <Image preview={this.state.preview} icon={this.props.file.icon || this.props.file.type} view={this.props.view} path={path.join(this.props.path, this.props.file.name)} />
<Filename filename={this.props.file.name} filesize={this.props.file.size} filetype={this.props.file.type} onRename={this.onRename.bind(this)} is_renaming={this.state.is_renaming} onRenameCancel={this.onRenameRequest.bind(this, false)}/> <Filename filename={this.props.file.name} filesize={this.props.file.size} filetype={this.props.file.type} onRename={this.onRename.bind(this)} is_renaming={this.state.is_renaming} onRenameCancel={this.onRenameRequest.bind(this, false)}/>
<DateTime show={this.state.icon !== 'loading'} timestamp={this.props.file.time} /> <DateTime show={this.state.icon !== 'loading'} timestamp={this.props.file.time} />
<ActionButton onClickRename={this.onRenameRequest.bind(this)} onClickDelete={this.onDeleteRequest.bind(this)} onClickShare={this.onShareRequest.bind(this)} is_renaming={this.state.is_renaming} can_rename={this.props.metadata.can_rename !== false} can_delete={this.props.metadata.can_delete !== false} /> <ActionButton onClickRename={this.onRenameRequest.bind(this)} onClickDelete={this.onDeleteRequest.bind(this)} onClickShare={this.onShareRequest.bind(this)} is_renaming={this.state.is_renaming} can_rename={this.props.metadata.can_rename !== false} can_delete={this.props.metadata.can_delete !== false} can_share={this.props.metadata.can_share !== false} />
</Card> </Card>
</Link> </Link>
</div> </div>
@ -310,7 +310,7 @@ const ActionButton = (props) => {
<NgIf cond={props.can_delete !== false} type="inline"> <NgIf cond={props.can_delete !== false} type="inline">
<Icon name="delete" onClick={onDelete} className="component_updater--icon"/> <Icon name="delete" onClick={onDelete} className="component_updater--icon"/>
</NgIf> </NgIf>
<NgIf cond={false && props.can_share !== false} type="inline"> <NgIf cond={props.can_share !== false} type="inline">
<Icon name="share" onClick={onShare} className="component_updater--icon"/> <Icon name="share" onClick={onShare} className="component_updater--icon"/>
</NgIf> </NgIf>
</div> </div>

View File

@ -2,40 +2,60 @@ import React from 'react';
import { Redirect } from 'react-router'; import { Redirect } from 'react-router';
import { Share } from '../model/'; import { Share } from '../model/';
import { notify } from '../helpers/'; import { notify, basename, filetype } from '../helpers/';
import { Loader, Input, Button, Container } from '../components/'; import { Loader, Input, Button, Container, ErrorPage, Icon, NgIf } from '../components/';
import './error.scss'; import './error.scss';
import './sharepage.scss';
@ErrorPage
export class SharePage extends React.Component { export class SharePage extends React.Component {
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {
redirection: null, path: null,
loading: true, key: null,
request_password: false, error: null,
request_username: false loading: false
}; };
} }
componentDidMount(){ componentDidMount(){
Share.get(this.props.match.params.id) this._proofQuery(this.props.match.params.id).then(() => {
.then((res) => { if(this.refs.$input) {
console.log(res); this.refs.$input.ref.focus();
this.setState({ }
loading: false, });
request_password: true
});
})
.catch((res) => {
this.setState({
loading: false
});
});
} }
submitProof(e, type, value){ submitProof(e, type, value){
e.preventDefault(); e.preventDefault();
console.log(type, value); this.setState({loading: true});
this._proofQuery(this.props.match.params.id, {type: type, value:value});
}
_proofQuery(id, data = {}){
this.setState({loading: true});
return Share.proof(id, data).then((res) => {
if(this.refs.$input) {
this.refs.$input.ref.value = "";
}
let st = {
key: res.key,
path: res.path || null,
share: res.id,
loading: false
};
if(res.message){
notify.send(res.message, "info");
}else if(res.error){
st.error = res.error;
window.setTimeout(() => this.setState({error: null}), 500);
}
return new Promise((done) => {
this.setState(st, () => done());
});
}).catch((err) => this.props.error(err));
} }
render() { render() {
@ -45,52 +65,60 @@ export class SharePage extends React.Component {
}; };
}; };
if(this.state.loading === true){ let className = this.state.error ? "error rand-"+Math.random().toString() : "";
return ( <div> <Loader /> </div> );
}
if(this.state.request_password === true){ if(this.state.path !== null){
return ( if(filetype(this.state.path) === "directory"){
<Container maxWidth="350px"> return ( <Redirect to={`/files/?share=${this.state.share}`} /> );
<form onSubmit={(e) => this.submitProof(e, "password", this.refs.$input.ref.value)} style={marginTop()}>
<Input ref="$input" type="password" placeholder="Password" />
<Button theme="emphasis">OK</Button>
</form>
</Container>
);
}else if(this.state.request_username === true){
return (
<Container maxWidth="350px">
<form onSubmit={(e) => this.submitProof(e, "email", this.refs.$input.ref.value)} style={marginTop()}>
<Input ref="$input" type="text" placeholder="Your email address" />
<Button theme="emphasis">OK</Button>
</form>
</Container>
);
}else if(this.state.request_verification === true){
return (
<Container maxWidth="350px">
<form onSubmit={(e) => this.submitProof(e, "code", this.refs.$input.ref.value)} style={marginTop()}>
<Input ref="$input" type="text" placeholder="Code" />
<Button theme="emphasis">OK</Button>
</form>
</Container>
);
}
if(this.state.redirection !== null){
if(this.state.redirection.slice(-1) === "/"){
return ( <Redirect to={"/files" + this.state.redirection} /> );
}else{ }else{
return ( <Redirect to={"/view" + this.state.redirection} /> ); return ( <Redirect to={`/view/${basename(this.state.path)}?share=${this.state.share}`} /> );
} }
}else{ } else if (this.state.key === null){
return ( return (
<div className="error-page"> <div style={marginTop()}>
<h1>Oops!</h1> <Loader />
<h2>There's nothing in here</h2>
</div> </div>
); );
} else if(this.state.key === "code"){
return (
<Container maxWidth="300px" className="sharepage_component">
<form className={className} onSubmit={(e) => this.submitProof(e, "code", this.refs.$input.ref.value)} style={marginTop()}>
<Input ref="$input" type="text" placeholder="Code" />
<Button theme="transparent">
<Icon name={this.state.loading ? "loading" : "arrow_right"}/>
</Button>
</form>
</Container>
);
} else if(this.state.key === "password"){
return (
<Container maxWidth="300px" className="sharepage_component">
<form className={className} onSubmit={(e) => this.submitProof(e, "password", this.refs.$input.ref.value)} style={marginTop()}>
<Input ref="$input" type="password" placeholder="Password" />
<Button theme="transparent">
<Icon name={this.state.loading ? "loading" : "arrow_right"}/>
</Button>
</form>
</Container>
);
}else if(this.state.key === "email"){
return (
<Container maxWidth="300px" className="sharepage_component">
<form className={className} onSubmit={(e) => this.submitProof(e, "email", this.refs.$input.ref.value)} style={marginTop()}>
<Input ref="$input" type="text" placeholder="Your email address" />
<Button theme="transparent">
<Icon name={this.state.loading ? "loading" : "arrow_right"}/>
</Button>
</form>
</Container>
);
} }
return (
<div className="error-page">
<h1>Oops!</h1>
<h2>There's nothing in here</h2>
</div>
);
} }
} }

View File

@ -0,0 +1,56 @@
.sharepage_component {
form {
display: flex;
background: white;
border-radius: 2px;
box-shadow: 2px 2px 2px rgba(0,0,0,0.05);
input {
padding: 15px 20px;
border-bottom: 0;
margin: 0;
}
button {
width: inherit;
padding: 0 10px;
.component_icon {
height: 25px;
}
}
}
.error{
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
animation: 0.2s ease-out 0s 1 enterZoomIn;
}
@keyframes enterZoomIn {
0% {
transform: scale(1.1)
}
100% {
transform: scale(1);
}
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}

View File

@ -309,7 +309,7 @@ class OrgViewer extends React.Component {
</div> </div>
); );
}) })
} }
</StickyContainer> </StickyContainer>
</NgIf> </NgIf>
</Modal> </Modal>

View File

@ -4,7 +4,7 @@ import { Link, withRouter } from 'react-router-dom';
import { Files } from '../../model/'; import { Files } from '../../model/';
import { sort } from '../../pages/filespage.helper.js'; import { sort } from '../../pages/filespage.helper.js';
import { Icon, NgIf, EventReceiver, EventEmitter } from '../../components/'; import { Icon, NgIf, EventReceiver, EventEmitter } from '../../components/';
import { dirname, basename, settings_get, getMimeType, debounce, gid } from '../../helpers/'; import { dirname, basename, settings_get, getMimeType, debounce, gid, appendShareToUrl } from '../../helpers/';
import './pager.scss'; import './pager.scss';
@ -25,7 +25,6 @@ export class Pager extends React.Component {
componentDidMount(){ componentDidMount(){
this.setNavigation(this.props); this.setNavigation(this.props);
window.addEventListener("keyup", this.navigate); window.addEventListener("keyup", this.navigate);
this.props.subscribe('media::next', () => { this.props.subscribe('media::next', () => {
this.navigatePage(this.calculateNextPageNumber(this.state.n)); this.navigatePage(this.calculateNextPageNumber(this.state.n));
}); });
@ -35,7 +34,7 @@ export class Pager extends React.Component {
} }
componentWillReceiveProps(props){ componentWillReceiveProps(props){
if(props.path === this.props.path){ if(props.path !== this.props.path){
this.setNavigation(props); this.setNavigation(props);
} }
} }
@ -57,7 +56,8 @@ export class Pager extends React.Component {
navigatePage(n){ navigatePage(n){
if(this.state.files[n]){ if(this.state.files[n]){
this.props.history.push(this.state.files[n].link+"?once="+gid()); const url = appendShareToUrl(this.state.files[n].link)
this.props.history.push(url);
if(this.refs.$page) this.refs.$page.blur(); if(this.refs.$page) this.refs.$page.blur();
let preload_index = (n >= this.state.n || (this.state.n === this.state.files.length - 1 && n === 0)) ? this.calculateNextPageNumber(n) : this.calculatePrevPageNumber(n); let preload_index = (n >= this.state.n || (this.state.n === this.state.files.length - 1 && n === 0)) ? this.calculateNextPageNumber(n) : this.calculatePrevPageNumber(n);
if(!this.state.files[preload_index].path){ if(!this.state.files[preload_index].path){
@ -79,6 +79,10 @@ export class Pager extends React.Component {
setNavigation(props){ setNavigation(props){
Files._ls_from_cache(dirname(props.path)) Files._ls_from_cache(dirname(props.path))
.then((f) => {
if(f === null) return Promise.reject({code: "NO_DATA"});
return Promise.resolve(f)
})
.then((f) => f.results.filter((file) => (isImage(file.name) || isVideo(file.name)) && file.type === "file")) .then((f) => f.results.filter((file) => (isImage(file.name) || isVideo(file.name)) && file.type === "file"))
.then((f) => sort(f, settings_get('filespage_sort') || 'type')) .then((f) => sort(f, settings_get('filespage_sort') || 'type'))
.then((f) => findPosition(f, basename(props.path))) .then((f) => findPosition(f, basename(props.path)))
@ -87,7 +91,8 @@ export class Pager extends React.Component {
files: res[0], files: res[0],
n: res[1] n: res[1]
}); });
}); })
.catch(() => {});
const findPosition = (files, filename) => { const findPosition = (files, filename) => {
let i; let i;

View File

@ -8,7 +8,7 @@ self.onmessage = function(message){
if(current_search != null){ if(current_search != null){
current_search.unsubscribe(); current_search.unsubscribe();
} }
current_search = Search(message.data.path, message.data.keyword).subscribe((a) => { current_search = Search([message.data.share, message.data.path], message.data.keyword).subscribe((a) => {
self.postMessage({type: "search::found", files: a}); self.postMessage({type: "search::found", files: a});
}, null, () => { }, null, () => {
self.postMessage({type: "search::completed"}) self.postMessage({type: "search::completed"})
@ -16,7 +16,7 @@ self.onmessage = function(message){
} }
} }
function Search(path, keyword){ function Search(key, keyword){
let results = []; let results = [];
return new Observable((obs) => { return new Observable((obs) => {
obs.next(results); obs.next(results);
@ -32,7 +32,7 @@ function Search(path, keyword){
results = results.concat(found); results = results.concat(found);
obs.next(results); obs.next(results);
} }
}, cache.FILE_PATH, path).then(() => { }, cache.FILE_PATH, [key[0], key[1]]).then(() => {
obs.complete(results); obs.complete(results);
}); });
}); });

View File

@ -15,6 +15,11 @@
"level": "INFO", "level": "INFO",
"telemetry": true "telemetry": true
}, },
"smtp": {
"addr": "smtp.gmail.com",
"username": "mickael.kerjean@gmail.com",
"password": "test"
},
"oauth": { "oauth": {
"gdrive": { "gdrive": {
"client_id": "", "client_id": "",

View File

@ -36,6 +36,13 @@ type Config struct {
Level string `json:"level"` Level string `json:"level"`
Telemetry bool `json:"telemetry"` Telemetry bool `json:"telemetry"`
} `json:"log"` } `json:"log"`
Email struct {
Server string `json:"server"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
From string `json:"from"`
} `json:"email"`
OAuthProvider struct { OAuthProvider struct {
Dropbox struct { Dropbox struct {
ClientID string `json:"client_id"` ClientID string `json:"client_id"`

View File

@ -1,6 +1,7 @@
package common package common
const ( const (
COOKIE_NAME = "auth" COOKIE_NAME_AUTH = "auth"
COOKIE_NAME_PROOF = "proof"
COOKIE_PATH = "/api/" COOKIE_PATH = "/api/"
) )

View File

@ -1,58 +1,144 @@
package common package common
import ( import (
"bytes"
"compress/zlib"
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/rand" "crypto/rand"
"crypto/sha1" "crypto/sha1"
"encoding/base32" "encoding/base32"
"encoding/base64" "encoding/base64"
"encoding/json"
"io" "io"
"io/ioutil"
mathrand "math/rand"
"math/big"
) )
func Encrypt(keystr string, text map[string]string) (string, error) { var Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
key := []byte(keystr)
plaintext, err := json.Marshal(text)
if err != nil {
return "", NewError("json marshalling: "+err.Error(), 500)
}
block, err := aes.NewCipher(key) func EncryptString(secret string, json string) (string, error) {
d, err := compress([]byte(json))
if err != nil { if err != nil {
return "", NewError("encryption issue (cipher): "+err.Error(), 500) return "", err
} }
ciphertext := make([]byte, aes.BlockSize+len(plaintext)) d, err = encrypt([]byte(secret), d)
iv := ciphertext[:aes.BlockSize] if err != nil {
if _, err := io.ReadFull(rand.Reader, iv); err != nil { return "", err
return "", NewError("encryption issue: "+err.Error(), 500)
} }
stream := cipher.NewCFBEncrypter(block, iv) return base64.URLEncoding.EncodeToString(d), nil
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
return base64.URLEncoding.EncodeToString(ciphertext), nil
} }
func Decrypt(keystr string, cryptoText string) (map[string]string, error) { func DecryptString(secret string, data string) (string, error){
var raw map[string]string d, err := base64.URLEncoding.DecodeString(data)
if err != nil {
key := []byte(keystr) return "", err
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
block, err := aes.NewCipher(key)
if err != nil || len(ciphertext) < aes.BlockSize {
return raw, NewError("Cipher is too short", 500)
} }
d, err = decrypt([]byte(secret), d)
iv := ciphertext[:aes.BlockSize] if err != nil {
ciphertext = ciphertext[aes.BlockSize:] return "", err
stream := cipher.NewCFBDecrypter(block, iv) }
stream.XORKeyStream(ciphertext, ciphertext) d, err = decompress(d)
if err != nil {
json.Unmarshal(ciphertext, &raw) return "", err
return raw, nil }
return string(d), nil
} }
func GenerateID(params map[string]string) string { func Hash(str string) string {
hasher := sha1.New()
hasher.Write([]byte(str))
return "sha1::" + base32.HexEncoding.EncodeToString(hasher.Sum(nil))
}
func RandomString(n int) string {
b := make([]rune, n)
for i := range b {
max := *big.NewInt(int64(len(Letters)))
r, err := rand.Int(rand.Reader, &max)
if err != nil {
b[i] = Letters[0]
} else {
b[i] = Letters[r.Int64()]
}
}
return string(b)
}
func QuickString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = Letters[mathrand.Intn(len(Letters))]
}
return string(b)
}
func encrypt(key []byte, plaintext []byte) ([]byte, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
func decrypt(key []byte, ciphertext []byte) ([]byte, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, NewError("ciphertext too short", 500)
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}
func compress(something []byte) ([]byte, error) {
var b bytes.Buffer
w := zlib.NewWriter(&b)
w.Write(something)
w.Close()
return b.Bytes(), nil
}
func decompress(something []byte) ([]byte, error) {
b := bytes.NewBuffer(something)
r, err := zlib.NewReader(b)
if err != nil {
return []byte(""), nil
}
r.Close()
return ioutil.ReadAll(r)
}
func sign(something []byte) ([]byte, error) {
return something, nil
}
func verify(something []byte) ([]byte, error) {
return something, nil
}
func GenerateID(params map[string]string) string {
p := "type =>" + params["type"] p := "type =>" + params["type"]
p += "host =>" + params["host"] p += "host =>" + params["host"]
p += "hostname =>" + params["hostname"] p += "hostname =>" + params["hostname"]
@ -63,7 +149,5 @@ func GenerateID(params map[string]string) string {
p += "endpoint =>" + params["endpoint"] p += "endpoint =>" + params["endpoint"]
p += "bearer =>" + params["bearer"] p += "bearer =>" + params["bearer"]
p += "token =>" + params["token"] p += "token =>" + params["token"]
hasher := sha1.New() return Hash(p)
hasher.Write([]byte(p))
return "sha1::" + base32.HexEncoding.EncodeToString(hasher.Sum(nil))
} }

View File

@ -5,18 +5,18 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestEncryptSomething(t *testing.T) { func TestEncryptString(t *testing.T) {
key := "test|test|test|test|test" key := "test|test|test|test|test"
text := "I'm some text"
d := make(map[string]string) a, err := EncryptString(key, text)
d["foo"] = "bar"
str, err := Encrypt(key, d)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, a)
assert.NotEqual(t, a, text)
data, err := Decrypt(key, str) b, err := DecryptString(key, a)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "bar", data["foo"]) assert.Equal(t, b, text)
} }
func TestIDGeneration(t *testing.T) { func TestIDGeneration(t *testing.T) {
@ -32,3 +32,11 @@ func TestIDGeneration(t *testing.T) {
assert.NotEqual(t, id1, id2) assert.NotEqual(t, id1, id2)
assert.Equal(t, id2, id3) assert.Equal(t, id2, id3)
} }
func TestStringGeneration(t *testing.T) {
str := QuickString(10)
str1 := QuickString(10)
str2 := QuickString(10)
assert.Equal(t, len(str), 10)
t.Log(str, str1, str2)
}

View File

@ -56,5 +56,7 @@ type Metadata struct {
CanRename *bool `json:"can_rename,omitempty"` CanRename *bool `json:"can_rename,omitempty"`
CanMove *bool `json:"can_move,omitempty"` CanMove *bool `json:"can_move,omitempty"`
CanUpload *bool `json:"can_upload,omitempty"` CanUpload *bool `json:"can_upload,omitempty"`
CanDelete *bool `json:"can_delete,omitempty"`
CanShare *bool `json:"can_share,omitempty"`
Expire *time.Time `json:"-"` Expire *time.Time `json:"-"`
} }

View File

@ -1,19 +1,5 @@
package common package common
import (
"math/rand"
)
var Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
func RandomString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = Letters[rand.Intn(len(Letters))]
}
return string(b)
}
func NewBool(t bool) *bool { func NewBool(t bool) *bool {
return &t return &t
} }
@ -35,13 +21,13 @@ func NewBoolFromInterface(val interface{}) bool {
default: return false default: return false
} }
} }
func NewIntpFromInterface(val interface{}) *int { func NewInt64pFromInterface(val interface{}) *int64 {
switch val.(type) { switch val.(type) {
case int: case int64:
v := val.(int) v := val.(int64)
return &v return &v
case float64: case float64:
v := int(val.(float64)) v := int64(val.(float64))
return &v return &v
default: return nil default: return nil
} }

View File

@ -3,6 +3,7 @@ package ctrl
import ( import (
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
"github.com/mickael-kerjean/nuage/server/services" "github.com/mickael-kerjean/nuage/server/services"
"github.com/mickael-kerjean/nuage/server/model"
"io" "io"
"net/http" "net/http"
"path/filepath" "path/filepath"
@ -18,6 +19,16 @@ type FileInfo struct {
} }
func FileLs(ctx App, res http.ResponseWriter, req *http.Request) { func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
var files []FileInfo
if model.CanRead(&ctx) == false {
if model.CanUpload(&ctx) == false {
SendErrorResult(res, NewError("Permission denied", 403))
return
}
SendSuccessResults(res, files)
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path")) path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil { if err != nil {
SendErrorResult(res, err) SendErrorResult(res, err)
@ -30,7 +41,6 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
return return
} }
files := []FileInfo{}
for _, entry := range entries { for _, entry := range entries {
f := FileInfo{ f := FileInfo{
Name: entry.Name(), Name: entry.Name(),
@ -48,10 +58,28 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
files = append(files, f) files = append(files, f)
} }
var perms *Metadata var perms *Metadata = &Metadata{}
if obj, ok := ctx.Backend.(interface{ Meta(path string) *Metadata }); ok { if obj, ok := ctx.Backend.(interface{ Meta(path string) *Metadata }); ok {
perms = obj.Meta(path) perms = obj.Meta(path)
} }
if model.CanEdit(&ctx) == false {
perms.CanCreateFile = NewBool(false)
perms.CanCreateDirectory = NewBool(false)
perms.CanRename = NewBool(false)
perms.CanMove = NewBool(false)
perms.CanDelete = NewBool(false)
}
if model.CanUpload(&ctx) == false {
perms.CanCreateDirectory = NewBool(false)
perms.CanRename = NewBool(false)
perms.CanMove = NewBool(false)
perms.CanDelete = NewBool(false)
}
if model.CanShare(&ctx) == false {
perms.CanShare = NewBool(false)
}
SendSuccessResultsWithMetadata(res, files, perms) SendSuccessResultsWithMetadata(res, files, perms)
} }
@ -62,6 +90,10 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
MaxAge: -1, MaxAge: -1,
Path: "/", Path: "/",
}) })
if model.CanRead(&ctx) == false {
SendErrorResult(res, NewError("Permission denied", 403))
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path")) path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil { if err != nil {
@ -87,6 +119,11 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
} }
func FileSave(ctx App, res http.ResponseWriter, req *http.Request) { func FileSave(ctx App, res http.ResponseWriter, req *http.Request) {
if model.CanEdit(&ctx) == false {
SendErrorResult(res, NewError("Permission denied", 403))
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path")) path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil { if err != nil {
SendErrorResult(res, err) SendErrorResult(res, err)
@ -112,6 +149,11 @@ func FileSave(ctx App, res http.ResponseWriter, req *http.Request) {
} }
func FileMv(ctx App, res http.ResponseWriter, req *http.Request) { func FileMv(ctx App, res http.ResponseWriter, req *http.Request) {
if model.CanEdit(&ctx) == false {
SendErrorResult(res, NewError("Permission denied", 403))
return
}
from, err := pathBuilder(ctx, req.URL.Query().Get("from")) from, err := pathBuilder(ctx, req.URL.Query().Get("from"))
if err != nil { if err != nil {
SendErrorResult(res, err) SendErrorResult(res, err)
@ -136,6 +178,11 @@ func FileMv(ctx App, res http.ResponseWriter, req *http.Request) {
} }
func FileRm(ctx App, res http.ResponseWriter, req *http.Request) { func FileRm(ctx App, res http.ResponseWriter, req *http.Request) {
if model.CanEdit(&ctx) == false {
SendErrorResult(res, NewError("Permission denied", 403))
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path")) path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil { if err != nil {
SendErrorResult(res, err) SendErrorResult(res, err)
@ -150,6 +197,11 @@ func FileRm(ctx App, res http.ResponseWriter, req *http.Request) {
} }
func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) { func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) {
if model.CanUpload(&ctx) == false {
SendErrorResult(res, NewError("Permission denied", 403))
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path")) path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil { if err != nil {
SendErrorResult(res, err) SendErrorResult(res, err)
@ -165,6 +217,11 @@ func FileMkdir(ctx App, res http.ResponseWriter, req *http.Request) {
} }
func FileTouch(ctx App, res http.ResponseWriter, req *http.Request) { func FileTouch(ctx App, res http.ResponseWriter, req *http.Request) {
if model.CanUpload(&ctx) == false {
SendErrorResult(res, NewError("Permission denied", 403))
return
}
path, err := pathBuilder(ctx, req.URL.Query().Get("path")) path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil { if err != nil {
SendErrorResult(res, err) SendErrorResult(res, err)

View File

@ -1,6 +1,7 @@
package ctrl package ctrl
import ( import (
"encoding/json"
"github.com/mickael-kerjean/mux" "github.com/mickael-kerjean/mux"
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
"github.com/mickael-kerjean/nuage/server/model" "github.com/mickael-kerjean/nuage/server/model"
@ -17,7 +18,7 @@ func SessionGet(ctx App, res http.ResponseWriter, req *http.Request) {
r := Session { r := Session {
IsAuth: false, IsAuth: false,
} }
if ctx.Backend == nil { if ctx.Backend == nil {
SendSuccessResult(res, r) SendSuccessResult(res, r)
return return
@ -63,13 +64,19 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
return return
} }
obfuscate, err := Encrypt(ctx.Config.General.SecretKey, session) s, err := json.Marshal(session);
if err != nil {
SendErrorResult(res, NewError(err.Error(), 500))
return
}
obfuscate, err := EncryptString(ctx.Config.General.SecretKey, string(s))
if err != nil { if err != nil {
SendErrorResult(res, NewError(err.Error(), 500)) SendErrorResult(res, NewError(err.Error(), 500))
return return
} }
cookie := http.Cookie{ cookie := http.Cookie{
Name: COOKIE_NAME, Name: COOKIE_NAME_AUTH,
Value: obfuscate, Value: obfuscate,
MaxAge: 60 * 60 * 24 * 30, MaxAge: 60 * 60 * 24 * 30,
Path: COOKIE_PATH, Path: COOKIE_PATH,
@ -88,7 +95,7 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
func SessionLogout(ctx App, res http.ResponseWriter, req *http.Request) { func SessionLogout(ctx App, res http.ResponseWriter, req *http.Request) {
cookie := http.Cookie{ cookie := http.Cookie{
Name: COOKIE_NAME, Name: COOKIE_NAME_AUTH,
Value: "", Value: "",
Path: COOKIE_PATH, Path: COOKIE_PATH,
MaxAge: -1, MaxAge: -1,

View File

@ -1,10 +1,13 @@
package ctrl package ctrl
import ( import (
"encoding/json"
"fmt"
"github.com/mickael-kerjean/mux" "github.com/mickael-kerjean/mux"
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
"github.com/mickael-kerjean/nuage/server/model" "github.com/mickael-kerjean/nuage/server/model"
"net/http" "net/http"
"strings"
) )
func ShareList(ctx App, res http.ResponseWriter, req *http.Request) { func ShareList(ctx App, res http.ResponseWriter, req *http.Request) {
@ -23,12 +26,64 @@ func ShareGet(ctx App, res http.ResponseWriter, req *http.Request) {
SendErrorResult(res, err) SendErrorResult(res, err)
return return
} }
SendSuccessResult(res, s) SendSuccessResult(res, struct{
Id string `json:"id"`
Path string `json:"path"`
}{
Id: s.Id,
Path: s.Path,
})
} }
func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) { func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
if model.CanShare(&ctx) == false {
SendErrorResult(res, NewError("No permission", 403))
return
}
s := extractParams(req, &ctx) s := extractParams(req, &ctx)
s.Path = NewStringFromInterface(ctx.Body["path"]) s.Path = NewStringFromInterface(ctx.Body["path"])
s.Auth = func(req *http.Request) string {
c, _ := req.Cookie(COOKIE_NAME_AUTH)
if c == nil {
return ""
}
var data map[string]string
str, err := DecryptString(ctx.Config.General.SecretKey, c.Value)
if err != nil {
return ""
}
if err = json.Unmarshal([]byte(str), &data); err != nil {
return ""
}
boolToString := func(b bool) string {
if b == true {
return "yes"
}
return "no"
}
data["path"] = func(p1 string, p2 string) string{
if p1 == "" {
return p2
}
return p1 + strings.TrimPrefix(p2, "/")
}(ctx.Session["path"], s.Path)
data["can_share"] = boolToString(s.CanShare)
data["can_read"] = boolToString(s.CanRead)
data["can_write"] = boolToString(s.CanWrite)
data["can_upload"] = boolToString(s.CanUpload)
s, err := json.Marshal(data);
if err != nil {
return ""
}
obfuscate, err := EncryptString(ctx.Config.General.SecretKey, string(s))
if err != nil {
return ""
}
return obfuscate
}(req)
if err := model.ShareUpsert(&s); err != nil { if err := model.ShareUpsert(&s); err != nil {
SendErrorResult(res, err) SendErrorResult(res, err)
@ -37,13 +92,91 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
SendSuccessResult(res, nil) SendSuccessResult(res, nil)
} }
func ShareGiveProof(ctx App, res http.ResponseWriter, req *http.Request) { func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) {
// switch NewStringFromInterface(ctx.Body["type"]) { var submittedProof model.Proof
// case "password": var verifiedProof []model.Proof
// case "code": nil var requiredProof []model.Proof
// case "email": nil var remainingProof []model.Proof
// } var s model.Share
SendSuccessResult(res, false)
// 1) initialise the current context
s = extractParams(req, &ctx)
if err := model.ShareGet(&s); err != nil {
SendErrorResult(res, err)
return
}
submittedProof = model.Proof{
Key: fmt.Sprint(ctx.Body["type"]),
Value: fmt.Sprint(ctx.Body["value"]),
}
verifiedProof = model.ShareProofGetAlreadyVerified(req, &ctx)
requiredProof = model.ShareProofGetRequired(s)
// 2) validate the current context
if len(verifiedProof) > 20 || len(requiredProof) > 20 {
SendErrorResult(res, NewError("Input error", 405))
return
}
if _, err := s.IsValid(); err != nil {
SendErrorResult(res, err)
return
}
// 3) process the proof sent by the user
submittedProof, err := model.ShareProofVerifier(&ctx, s, submittedProof);
if err != nil {
submittedProof.Error = NewString(err.Error())
SendSuccessResult(res, submittedProof)
return
}
if submittedProof.Key == "code" {
submittedProof.Value = ""
submittedProof.Message = NewString("We've sent you a message with a verification code")
SendSuccessResult(res, submittedProof)
return
}
if submittedProof.Key != "<nil>" {
submittedProof.Id = Hash(submittedProof.Key + "::" + submittedProof.Value)
verifiedProof = append(verifiedProof, submittedProof)
}
// 4) Find remaining proofs: requiredProof - verifiedProof
remainingProof = model.ShareProofCalculateRemainings(requiredProof, verifiedProof)
// log.Println("============")
// log.Println("REQUIRED: ", requiredProof)
// log.Println("SUBMITTED: ", submittedProof)
// log.Println("VERIFIED: ", verifiedProof)
// log.Println("REMAINING: ", remainingProof)
// log.Println("============")
// 5) persist proofs in client cookie
cookie := http.Cookie{
Name: COOKIE_NAME_PROOF,
Value: func(p []model.Proof) string {
j, _ := json.Marshal(p)
str, _ := EncryptString(ctx.Config.General.SecretKey, string(j))
return str
}(verifiedProof),
Path: COOKIE_PATH,
MaxAge: 60 * 60 * 24 * 30,
HttpOnly: true,
}
http.SetCookie(res, &cookie)
if len(remainingProof) > 0 {
SendSuccessResult(res, remainingProof[0])
return
}
SendSuccessResult(res, struct {
Id string `json:"id"`
Path string `json:"path"`
}{
Id: s.Id,
Path: "/",
})
} }
func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) { func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) {
@ -58,17 +191,18 @@ func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) {
func extractParams(req *http.Request, ctx *App) model.Share { func extractParams(req *http.Request, ctx *App) model.Share {
return model.Share{ return model.Share{
Id: NewStringFromInterface(mux.Vars(req)["id"]), Auth: "",
Backend: NewStringFromInterface(GenerateID(ctx.Session)), Id: NewStringFromInterface(mux.Vars(req)["id"]),
Path: NewStringFromInterface(req.URL.Query().Get("path")), Backend: NewStringFromInterface(GenerateID(ctx.Session)),
Password: NewStringpFromInterface(ctx.Body["password"]), Path: NewStringFromInterface(req.URL.Query().Get("path")),
Users: NewStringpFromInterface(ctx.Body["users"]), Password: NewStringpFromInterface(ctx.Body["password"]),
Expire: NewIntpFromInterface(ctx.Body["expire"]), Users: NewStringpFromInterface(ctx.Body["users"]),
Url: NewStringpFromInterface(ctx.Body["url"]), Expire: NewInt64pFromInterface(ctx.Body["expire"]),
Url: NewStringpFromInterface(ctx.Body["url"]),
CanManageOwn: NewBoolFromInterface(ctx.Body["can_manage_own"]), CanManageOwn: NewBoolFromInterface(ctx.Body["can_manage_own"]),
CanShare: NewBoolFromInterface(ctx.Body["can_share"]), CanShare: NewBoolFromInterface(ctx.Body["can_share"]),
CanRead: NewBoolFromInterface(ctx.Body["can_read"]), CanRead: NewBoolFromInterface(ctx.Body["can_read"]),
CanWrite: NewBoolFromInterface(ctx.Body["can_write"]), CanWrite: NewBoolFromInterface(ctx.Body["can_write"]),
CanUpload: NewBoolFromInterface(ctx.Body["can_upload"]), CanUpload: NewBoolFromInterface(ctx.Body["can_upload"]),
} }
} }

View File

@ -6,6 +6,7 @@ import (
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
"path/filepath" "path/filepath"
"os" "os"
"time"
) )
var DB *sql.DB var DB *sql.DB
@ -16,19 +17,33 @@ func init() {
cachePath := filepath.Join(GetCurrentDir(), DBCachePath) cachePath := filepath.Join(GetCurrentDir(), DBCachePath)
os.MkdirAll(cachePath, os.ModePerm) os.MkdirAll(cachePath, os.ModePerm)
var err error var err error
DB, err = sql.Open("sqlite3", cachePath+"/db.sql?_fk=true") if DB, err = sql.Open("sqlite3", cachePath+"/db.sql?_fk=true"); err != nil {
if err != nil {
return return
} }
stmt, err := DB.Prepare("CREATE TABLE IF NOT EXISTS Location(backend VARCHAR(16), path VARCHAR(512), CONSTRAINT pk_location PRIMARY KEY(backend, path))")
if err != nil {
return
}
stmt.Exec()
stmt, err = DB.Prepare("CREATE TABLE IF NOT EXISTS Share(id VARCHAR(64) PRIMARY KEY, related_backend VARCHAR(16), related_path VARCHAR(512), params JSON, FOREIGN KEY (related_backend, related_path) REFERENCES Location(backend, path) ON UPDATE CASCADE ON DELETE CASCADE)") if stmt, err := DB.Prepare("CREATE TABLE IF NOT EXISTS Location(backend VARCHAR(16), path VARCHAR(512), CONSTRAINT pk_location PRIMARY KEY(backend, path))"); err == nil {
if err != nil { stmt.Exec()
return
} }
stmt.Exec()
if stmt, err := DB.Prepare("CREATE TABLE IF NOT EXISTS Share(id VARCHAR(64) PRIMARY KEY, related_backend VARCHAR(16), related_path VARCHAR(512), params JSON, auth VARCHAR(4093) NOT NULL, FOREIGN KEY (related_backend, related_path) REFERENCES Location(backend, path) ON UPDATE CASCADE ON DELETE CASCADE)"); err == nil {
stmt.Exec()
}
if stmt, err := DB.Prepare("CREATE TABLE IF NOT EXISTS Verification(key VARCHAR(512), code VARCHAR(4), expire DATETIME DEFAULT (datetime('now', '+10 minutes')))"); err == nil {
stmt.Exec()
if stmt, err = DB.Prepare("CREATE INDEX idx_verification ON Verification(code, expire)"); err == nil {
stmt.Exec()
}
}
go func(){
autovacuum()
}()
}
func autovacuum() {
if stmt, err := DB.Prepare("DELETE FROM Verification WHERE expire < datetime('now')"); err == nil {
stmt.Exec()
}
time.Sleep(6 * time.Hour)
} }

View File

@ -1,9 +1,38 @@
package model package model
func CanRemoveShare() bool { import (
. "github.com/mickael-kerjean/nuage/server/common"
)
func CanRead(ctx *App) bool {
keyword := ctx.Session["can_read"]
if keyword == "" || keyword == "yes" {
return true
}
return false return false
} }
func CanEditShare() bool { func CanEdit(ctx *App) bool {
keyword := ctx.Session["can_write"]
if keyword == "" || keyword == "yes" {
return true
}
return false
}
func CanUpload(ctx *App) bool {
keyword := ctx.Session["can_upload"]
if keyword == "" || keyword == "yes" {
return true
}
return false
}
func CanShare(ctx *App) bool {
keyword := ctx.Session["can_share"]
if keyword == "" || keyword == "yes" {
return true
}
return false return false
} }

View File

@ -2,21 +2,38 @@ package model
import ( import (
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
"bytes"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"log"
"net/http"
"net/smtp"
"html/template"
"strings"
"time"
) )
const PASSWORD_DUMMY = "{{PASSWORD}}" const PASSWORD_DUMMY = "{{PASSWORD}}"
type Proof struct {
Id string `json:"id"`
Key string `json:"key"`
Value string `json:"-"`
Message *string `json:"message,omitempty"`
Error *string `json:"error,omitempty"`
}
type Share struct { type Share struct {
Id string `json:"id"` Id string `json:"id"`
Backend string `json:"-"` Backend string `json:"-"`
Auth string `json:"auth,omitempty"`
Path string `json:"path"` Path string `json:"path"`
Password *string `json:"password,omitempty"` Password *string `json:"password,omitempty"`
Users *string `json:"users,omitempty"` Users *string `json:"users,omitempty"`
Expire *int `json:"expire,omitempty"` Expire *int64 `json:"expire,omitempty"`
Url *string `json:"url,omitempty"` Url *string `json:"url,omitempty"`
CanShare bool `json:"can_share"` CanShare bool `json:"can_share"`
CanManageOwn bool `json:"can_manage_own"` CanManageOwn bool `json:"can_manage_own"`
@ -25,10 +42,27 @@ type Share struct {
CanUpload bool `json:"can_upload"` CanUpload bool `json:"can_upload"`
} }
func NewShare(id string) Share {
return Share{
Id: id,
}
}
func (s Share) IsValid() (bool, error) {
if s.Expire != nil {
now := time.Now().UnixNano() / 1000000
if now > *s.Expire {
return false, NewError("Link has expired", 410)
}
}
return true, nil
}
func (s *Share) MarshalJSON() ([]byte, error) { func (s *Share) MarshalJSON() ([]byte, error) {
p := Share{ p := Share{
s.Id, s.Id,
s.Backend, s.Backend,
"",
s.Path, s.Path,
func(pass *string) *string{ func(pass *string) *string{
if pass != nil { if pass != nil {
@ -57,7 +91,7 @@ func(s *Share) UnmarshallJSON(b []byte) error {
switch key { switch key {
case "password": s.Password = NewStringpFromInterface(value) case "password": s.Password = NewStringpFromInterface(value)
case "users": s.Users = NewStringpFromInterface(value) case "users": s.Users = NewStringpFromInterface(value)
case "expire": s.Expire = NewIntpFromInterface(value) case "expire": s.Expire = NewInt64pFromInterface(value)
case "url": s.Url = NewStringpFromInterface(value) case "url": s.Url = NewStringpFromInterface(value)
case "can_share": s.CanShare = NewBoolFromInterface(value) case "can_share": s.CanShare = NewBoolFromInterface(value)
case "can_manage_own": s.CanManageOwn = NewBoolFromInterface(value) case "can_manage_own": s.CanManageOwn = NewBoolFromInterface(value)
@ -70,11 +104,11 @@ func(s *Share) UnmarshallJSON(b []byte) error {
} }
func ShareList(p *Share) ([]Share, error) { func ShareList(p *Share) ([]Share, error) {
stmt, err := DB.Prepare("SELECT id, related_path, params FROM Share WHERE related_backend = ?") stmt, err := DB.Prepare("SELECT id, related_path, params FROM Share WHERE related_backend = ? AND related_path LIKE ? || '%' ")
if err != nil { if err != nil {
return nil, err return nil, err
} }
rows, err := stmt.Query(p.Backend) rows, err := stmt.Query(p.Backend, p.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -83,7 +117,7 @@ func ShareList(p *Share) ([]Share, error) {
var a Share var a Share
var params []byte var params []byte
rows.Scan(&a.Id, &a.Path, &params) rows.Scan(&a.Id, &a.Path, &params)
json.Unmarshal(params, &a) json.Unmarshal(params, &a)
sharedFiles = append(sharedFiles, a) sharedFiles = append(sharedFiles, a)
} }
rows.Close() rows.Close()
@ -91,26 +125,16 @@ func ShareList(p *Share) ([]Share, error) {
} }
func ShareGet(p *Share) error { func ShareGet(p *Share) error {
if err := shareGet(p); err != nil { stmt, err := DB.Prepare("SELECT id, related_path, params, auth FROM share WHERE id = ?")
return err
}
if p.Password != nil {
p.Password = NewString(PASSWORD_DUMMY)
}
return nil
}
func shareGet(p *Share) error {
stmt, err := DB.Prepare("SELECT id, related_path, params FROM share WHERE id = ?")
if err != nil { if err != nil {
return err return err
} }
defer stmt.Close() defer stmt.Close()
row := stmt.QueryRow(p.Id) row := stmt.QueryRow(p.Id)
var str []byte var str []byte
if err = row.Scan(&p.Id, &p.Path, &str); err != nil { if err = row.Scan(&p.Id, &p.Path, &str, &p.Auth); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return NewError("No Result", 404) return NewError("Not Found", 404)
} }
return err return err
} }
@ -123,7 +147,7 @@ func ShareUpsert(p *Share) error {
if *p.Password == PASSWORD_DUMMY { if *p.Password == PASSWORD_DUMMY {
var copy Share var copy Share
copy.Id = p.Id copy.Id = p.Id
shareGet(&copy); ShareGet(&copy);
p.Password = copy.Password p.Password = copy.Password
} else { } else {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(*p.Password), bcrypt.DefaultCost) hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(*p.Password), bcrypt.DefaultCost)
@ -146,14 +170,14 @@ func ShareUpsert(p *Share) error {
} }
} }
stmt, err = DB.Prepare("INSERT INTO Share(id, related_backend, related_path, params) VALUES($1, $2, $3, $4) ON CONFLICT(id) DO UPDATE SET related_backend = $2, related_path = $3, params = $4") stmt, err = DB.Prepare("INSERT INTO Share(id, related_backend, related_path, params, auth) VALUES($1, $2, $3, $4, $5) ON CONFLICT(id) DO UPDATE SET related_backend = $2, related_path = $3, params = $4")
if err != nil { if err != nil {
return err return err
} }
j, _ := json.Marshal(&struct { j, _ := json.Marshal(&struct {
Password *string `json:"password,omitempty"` Password *string `json:"password,omitempty"`
Users *string `json:"users,omitempty"` Users *string `json:"users,omitempty"`
Expire *int `json:"expire,omitempty"` Expire *int64 `json:"expire,omitempty"`
Url *string `json:"url,omitempty"` Url *string `json:"url,omitempty"`
CanShare bool `json:"can_share"` CanShare bool `json:"can_share"`
CanManageOwn bool `json:"can_manage_own"` CanManageOwn bool `json:"can_manage_own"`
@ -171,7 +195,7 @@ func ShareUpsert(p *Share) error {
CanWrite: p.CanWrite, CanWrite: p.CanWrite,
CanUpload: p.CanUpload, CanUpload: p.CanUpload,
}) })
_, err = stmt.Exec(p.Id, p.Backend, p.Path, j) _, err = stmt.Exec(p.Id, p.Backend, p.Path, j, p.Auth)
return err return err
} }
@ -183,3 +207,461 @@ func ShareDelete(p *Share) error {
_, err = stmt.Exec(p.Id, p.Backend) _, err = stmt.Exec(p.Id, p.Backend)
return err return err
} }
func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
p := proof
if proof.Key == "password" {
if s.Password == nil {
return p, NewError("No password required", 400)
}
time.Sleep(1000 * time.Millisecond)
if err := bcrypt.CompareHashAndPassword([]byte(*s.Password), []byte(proof.Value)); err != nil {
return p, NewError("Invalid Password", 403)
}
p.Value = *s.Password
}
if proof.Key == "email" {
// find out if user is authorized
if s.Users == nil {
return p, NewError("Authentication not required", 400)
}
var user *string
for _, possibleUser := range strings.Split(*s.Users, ",") {
if proof.Value == strings.Trim(possibleUser, " ") {
user = &proof.Value
}
}
if user == nil {
time.Sleep(1000 * time.Millisecond)
return p, NewError("No access was provided", 400)
}
// prepare the verification code
stmt, err := DB.Prepare("INSERT INTO Verification(key, code) VALUES(?, ?)");
if err != nil {
return p, err
}
code := RandomString(4)
if _, err := stmt.Exec("email::" + proof.Value, code); err != nil {
return p, err
}
// Prepare message
var b bytes.Buffer
t := template.New("email")
t.Parse(TmplEmailVerification())
t.Execute(&b, struct{
Code string
}{code})
p.Key = "code"
p.Value = ""
p.Message = NewString("We've sent you a message with a verification code")
// Send email
addr := fmt.Sprintf("%s:%d", ctx.Config.Email.Server, ctx.Config.Email.Port)
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
subject := "Subject: Your verification code\n"
msg := []byte(subject + mime + "\n" + b.String())
auth := smtp.PlainAuth("", ctx.Config.Email.Username, ctx.Config.Email.Password, ctx.Config.Email.Server)
if err := smtp.SendMail(addr, auth, ctx.Config.Email.From, []string{"mickael@kerjean.me"}, msg); err != nil {
log.Println("ERROR: ", err)
log.Println("Verification code: " + code)
return p, NewError("Couldn't send email", 500)
}
}
if proof.Key == "code" {
// find key for given code
stmt, err := DB.Prepare("SELECT key FROM Verification WHERE code = ? AND expire > datetime('now')")
if err != nil {
return p, NewError("Not found", 404)
}
row := stmt.QueryRow(proof.Value)
var key string
if err = row.Scan(&key); err != nil {
if err == sql.ErrNoRows {
stmt.Close()
p.Key = "email"
p.Value = ""
return p, NewError("Not found", 404)
}
stmt.Close()
return p, err
}
stmt.Close()
// cleanup current attempt so that it isn't used for malicious purpose
if stmt, err = DB.Prepare("DELETE FROM Verification WHERE code = ?"); err == nil {
stmt.Exec(proof.Value)
stmt.Close()
}
p.Key = "email"
p.Value = strings.TrimPrefix(key, "email::")
}
return p, nil
}
func ShareProofGetAlreadyVerified(req *http.Request, ctx *App) []Proof {
var p []Proof
var cookieValue string
c, _ := req.Cookie(COOKIE_NAME_PROOF)
if c == nil {
return p
}
cookieValue = c.Value
if len(cookieValue) > 500 {
return p
}
j, err := DecryptString(ctx.Config.General.SecretKey, cookieValue)
if err != nil {
return p
}
_ = json.Unmarshal([]byte(j), &p)
return p
}
func ShareProofGetRequired(s Share) []Proof {
var p []Proof
if s.Password != nil {
p = append(p, Proof{Key: "password", Value: *s.Password})
}
if s.Users != nil {
p = append(p, Proof{Key: "email", Value: *s.Users})
}
return p
}
func ShareProofCalculateRemainings(ref []Proof, mem []Proof) []Proof {
var remainingProof []Proof
for i := 0; i < len(ref); i++ {
keep := true
for j := 0; j < len(mem); j++ {
if shareProofAreEquivalent(ref[i], mem[j]) {
keep = false
break;
}
}
if keep {
remainingProof = append(remainingProof, ref[i])
}
}
return remainingProof
}
func shareProofAreEquivalent(ref Proof, p Proof) bool {
if ref.Key != p.Key {
return false
}
for _, chunk := range strings.Split(ref.Value, ",") {
chunk = strings.Trim(chunk, " ")
if p.Id == Hash(ref.Key + "::" + chunk) {
return true
}
}
return false
}
func TmplEmailVerification() string {
return `
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Nuage code</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%; }
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; }
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top; }
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%; }
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 450px;
padding: 10px;
width: 580px; }
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 450px;
padding: 10px; }
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%; }
.wrapper {
box-sizing: border-box;
padding: 20px; }
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%; }
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center; }
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
margin-bottom: 30px; }
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize; }
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px; }
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px; }
a {
color: #3498db;
text-decoration: underline; }
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto; }
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center; }
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize; }
.btn-primary table td {
background-color: #3498db; }
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff; }
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0; }
.first {
margin-top: 0; }
.align-center {
text-align: center; }
.align-right {
text-align: right; }
.align-left {
text-align: left; }
.clear {
clear: both; }
.mt0 {
margin-top: 0; }
.mb0 {
margin-bottom: 0; }
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0; }
.powered-by a {
text-decoration: none; }
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0; }
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 490px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important; }
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important; }
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important; }
table[class=body] .content {
padding: 0 !important; }
table[class=body] .container {
padding: 0 !important;
width: 100% !important; }
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important; }
table[class=body] .btn table {
width: 100% !important; }
table[class=body] .btn a {
width: 100% !important; }
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important; }}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%; }
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%; }
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important; }
.btn-primary table td:hover {
background-color: #34495e !important; }
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important; } }
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">Your code to login</span>
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<h2 style="font-weight:100;margin:0">Your verification code is: <strong>{{.Code}}</strong></h2>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block powered-by">
Powered by <a href="http://github.com/mickael-kerjean/nuage">Nuage</a>.
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>
`
}

View File

@ -1,13 +1,12 @@
package ctrl package model
import ( import (
"testing" "testing"
"github.com/mickael-kerjean/nuage/server/model"
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var shareObj = model.Share{ var shareObj = Share{
Backend: "foo", Backend: "foo",
Id: "foo", Id: "foo",
Path: "/var/www/", Path: "/var/www/",
@ -16,7 +15,7 @@ var shareObj = model.Share{
CanRead: true, CanRead: true,
CanManageOwn: true, CanManageOwn: true,
CanShare: true, CanShare: true,
Expire: NewInt(1537759505787), Expire: NewInt64(1537759505787),
} }
@ -24,28 +23,28 @@ var shareObj = model.Share{
//// UPSERT //// UPSERT
func TestShareSimpleUpsert(t *testing.T) { func TestShareSimpleUpsert(t *testing.T) {
err := model.ShareUpsert(&shareObj); err := ShareUpsert(&shareObj);
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestShareMultipleUpsert(t *testing.T) { func TestShareMultipleUpsert(t *testing.T) {
err := model.ShareUpsert(&shareObj); err := ShareUpsert(&shareObj);
assert.NoError(t, err) assert.NoError(t, err)
err = model.ShareUpsert(&shareObj); err = ShareUpsert(&shareObj);
assert.NoError(t, err) assert.NoError(t, err)
err = model.ShareGet(&shareObj) err = ShareGet(&shareObj)
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestShareUpsertIsProperlyInserted(t *testing.T) { func TestShareUpsertIsProperlyInserted(t *testing.T) {
err := model.ShareUpsert(&shareObj); err := ShareUpsert(&shareObj);
assert.NoError(t, err) assert.NoError(t, err)
var obj model.Share var obj Share
obj.Id = "foo" obj.Id = "foo"
err = model.ShareGet(&obj) err = ShareGet(&obj)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, obj.Password) assert.NotNil(t, obj.Password)
} }
@ -54,28 +53,28 @@ func TestShareUpsertIsProperlyInserted(t *testing.T) {
//// get //// get
func TestShareGetNonExisting(t *testing.T) { func TestShareGetNonExisting(t *testing.T) {
var s model.Share = shareObj var s Share = shareObj
s.Id = "nothing" s.Id = "nothing"
err := model.ShareGet(&s); err := ShareGet(&s);
assert.Error(t, err, "Shouldn't be able to get something that doesn't exist yet") assert.Error(t, err, "Shouldn't be able to get something that doesn't exist yet")
} }
func TestShareGetExisting(t *testing.T) { func TestShareGetExisting(t *testing.T) {
err := model.ShareUpsert(&shareObj); err := ShareUpsert(&shareObj);
assert.NoError(t, err, "Upsert issue") assert.NoError(t, err, "Upsert issue")
err = model.ShareGet(&shareObj); err = ShareGet(&shareObj);
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestShareGetExistingMakeSureDataIsOk(t *testing.T) { func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
err := model.ShareUpsert(&shareObj); err := ShareUpsert(&shareObj);
assert.NoError(t, err, "Upsert issue") assert.NoError(t, err, "Upsert issue")
var obj model.Share var obj Share
obj.Id = "foo" obj.Id = "foo"
obj.Backend = shareObj.Backend obj.Backend = shareObj.Backend
err = model.ShareGet(&obj); err = ShareGet(&obj);
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "foo", obj.Id) assert.Equal(t, "foo", obj.Id)
assert.Equal(t, "/var/www/", obj.Path) assert.Equal(t, "/var/www/", obj.Path)
@ -85,8 +84,7 @@ func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
assert.Equal(t, false, obj.CanWrite) assert.Equal(t, false, obj.CanWrite)
assert.Equal(t, false, obj.CanUpload) assert.Equal(t, false, obj.CanUpload)
assert.Equal(t, "foo", obj.Backend) assert.Equal(t, "foo", obj.Backend)
assert.Equal(t, shareObj.Expire, obj.Expire) assert.Equal(t, shareObj.Expire, obj.Expire)
assert.Equal(t, "{{PASSWORD}}", *obj.Password)
} }
////////////////////////////////////////////// //////////////////////////////////////////////
@ -94,11 +92,11 @@ func TestShareGetExistingMakeSureDataIsOk(t *testing.T) {
func TestShareListAll(t *testing.T) { func TestShareListAll(t *testing.T) {
// Initialise test // Initialise test
err := model.ShareUpsert(&shareObj); err := ShareUpsert(&shareObj);
assert.NoError(t, err, "Upsert issue") assert.NoError(t, err, "Upsert issue")
// Actual test // Actual test
list, err := model.ShareList(&shareObj) list, err := ShareList(&shareObj)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, list, 1) assert.Len(t, list, 1)
assert.NotNil(t, list[0].Password) assert.NotNil(t, list[0].Password)
@ -110,15 +108,49 @@ func TestShareListAll(t *testing.T) {
func TestShareDeleteShares(t *testing.T) { func TestShareDeleteShares(t *testing.T) {
// Initialise test // Initialise test
err := model.ShareUpsert(&shareObj); err := ShareUpsert(&shareObj);
assert.NoError(t, err, "Upsert issue") assert.NoError(t, err, "Upsert issue")
err = model.ShareGet(&shareObj) err = ShareGet(&shareObj)
assert.NoError(t, err)
// Actual Test
err = model.ShareDelete(&shareObj);
assert.NoError(t, err) assert.NoError(t, err)
err = model.ShareGet(&shareObj) // Actual Test
assert.Error(t, err) err = ShareDelete(&shareObj);
assert.NoError(t, err)
err = ShareGet(&shareObj)
assert.Error(t, err)
}
//////////////////////////////////////////////
//// PROOF
func TestShareVerifyEquivalence(t *testing.T) {
p1 := Proof {
Key: "password",
Value: "I'm something random",
}
p2 := Proof {
Key: p1.Key,
Id: "hash",
}
res := ShareProofAreEquivalent(p1, p2)
assert.Equal(t, false, res)
p2.Id = Hash(p1.Key + "::" + p1.Value)
res = ShareProofAreEquivalent(p1, p2)
assert.Equal(t, true, res)
p2.Key = "email"
res = ShareProofAreEquivalent(p1, p2)
assert.Equal(t, false, res)
p1.Key = "email"
p1.Value = "test@gmail.com,polo@gmail.com,jean@gmail.com"
p2.Key = "email"
p2.Id = Hash(p1.Key + "::" + "polo@gmail.com")
res = ShareProofAreEquivalent(p1, p2)
assert.Equal(t, true, res)
} }

View File

@ -33,8 +33,8 @@ func Init(a *App) *http.Server {
share.HandleFunc("/{id}", APIHandler(ShareGet, *a)).Methods("GET") share.HandleFunc("/{id}", APIHandler(ShareGet, *a)).Methods("GET")
share.HandleFunc("/{id}", APIHandler(ShareUpsert, *a)).Methods("POST") share.HandleFunc("/{id}", APIHandler(ShareUpsert, *a)).Methods("POST")
share.HandleFunc("/{id}", APIHandler(ShareDelete, *a)).Methods("DELETE") share.HandleFunc("/{id}", APIHandler(ShareDelete, *a)).Methods("DELETE")
r.HandleFunc("/api/proof", APIHandler(ShareGiveProof, *a)).Methods("GET") share.HandleFunc("/{id}/proof", APIHandler(ShareVerifyProof, *a)).Methods("POST")
// APP // APP
r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a)).Methods("GET") r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a)).Methods("GET")
r.PathPrefix("/assets").Handler(StaticHandler("./data/public/", *a)).Methods("GET") r.PathPrefix("/assets").Handler(StaticHandler("./data/public/", *a)).Methods("GET")

View File

@ -17,6 +17,7 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
start := time.Now() start := time.Now()
ctx.Body, _ = extractBody(req) ctx.Body, _ = extractBody(req)
ctx.Session, _ = extractSession(req, &ctx) ctx.Session, _ = extractSession(req, &ctx)
ctx.Backend, _ = extractBackend(req, &ctx) ctx.Backend, _ = extractBackend(req, &ctx)
res.Header().Add("Content-Type", "application/json") res.Header().Add("Content-Type", "application/json")
@ -65,11 +66,36 @@ func extractBody(req *http.Request) (map[string]interface{}, error) {
} }
func extractSession(req *http.Request, ctx *App) (map[string]string, error) { func extractSession(req *http.Request, ctx *App) (map[string]string, error) {
cookie, err := req.Cookie(COOKIE_NAME) var str string
if err != nil { var res map[string]string
return make(map[string]string), err
if req.URL.Query().Get("share") != "" {
s := model.NewShare(req.URL.Query().Get("share"))
if err := model.ShareGet(&s); err != nil {
return make(map[string]string), err
}
if _, err := s.IsValid(); err != nil {
return make(map[string]string), err
}
var verifiedProof []model.Proof = model.ShareProofGetAlreadyVerified(req, ctx)
var requiredProof []model.Proof = model.ShareProofGetRequired(s)
var remainingProof []model.Proof = model.ShareProofCalculateRemainings(requiredProof, verifiedProof)
if len(remainingProof) != 0 {
return make(map[string]string), NewError("Unauthorized Shared space", 400)
}
str = s.Auth
} else {
cookie, err := req.Cookie(COOKIE_NAME_AUTH)
if err != nil {
return make(map[string]string), err
}
str = cookie.Value
} }
return Decrypt(ctx.Config.General.SecretKey, cookie.Value)
str, err := DecryptString(ctx.Config.General.SecretKey, str)
err = json.Unmarshal([]byte(str), &res)
return res, err
} }
func extractBackend(req *http.Request, ctx *App) (IBackend, error) { func extractBackend(req *http.Request, ctx *App) (IBackend, error) {

View File

@ -41,7 +41,7 @@ func ProcessFileBeforeSend(reader io.Reader, ctx *App, req *http.Request, res *h
///////////////////////// /////////////////////////
// Specify transformation // Specify transformation
transform := &images.Transform{ transform := &images.Transform{
Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + RandomString(10)), Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + QuickString(10)),
Size: 300, Size: 300,
Crop: true, Crop: true,
Quality: 50, Quality: 50,