diff --git a/client/assets/css/reset.scss b/client/assets/css/reset.scss index 8f0c80a2..4ac74659 100644 --- a/client/assets/css/reset.scss +++ b/client/assets/css/reset.scss @@ -50,11 +50,15 @@ --secondary: #466372; --emphasis-secondary: #466372; --light: #909090; +<<<<<<< HEAD <<<<<<< HEAD --super-light: #f4f4f4; ======= --super-light: #F7F9FA; >>>>>>> 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; --success: #63d9b1; --dark: #313538; diff --git a/client/components/breadcrumb.js b/client/components/breadcrumb.js index e8cead54..f5adf5e6 100644 --- a/client/components/breadcrumb.js +++ b/client/components/breadcrumb.js @@ -45,7 +45,7 @@ export class BreadCrumb extends React.Component { } 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; return ( @@ -136,7 +136,7 @@ export class PathElementWrapper extends React.Component { return (
  • - + {this.limitSize(this.props.path.label)} diff --git a/client/components/button.scss b/client/components/button.scss index 18c981af..369648e7 100644 --- a/client/components/button.scss +++ b/client/components/button.scss @@ -23,4 +23,6 @@ button{ background: var(--emphasis); color: white } + &.transparent{ + } } diff --git a/client/components/container.js b/client/components/container.js index 89206968..e010c367 100644 --- a/client/components/container.js +++ b/client/components/container.js @@ -9,8 +9,10 @@ export class Container extends React.Component { } render() { const style = this.props.maxWidth ? {maxWidth: this.props.maxWidth} : {}; + let className = "component_container"; + if(this.props.className) className += " "+this.props.className; return ( -
    +
    {this.props.children}
    ); diff --git a/client/components/decorator.js b/client/components/decorator.js index 99f47dc7..d90dec3e 100644 --- a/client/components/decorator.js +++ b/client/components/decorator.js @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { Session } from '../model/'; import { Container, Loader, Icon } from '../components/'; -import { memory } from '../helpers/'; +import { memory, currentShare } from '../helpers/'; import '../pages/error.scss'; @@ -19,7 +19,7 @@ export function LoggedInOnly(WrappedComponent){ } componentDidMount(){ - if(this.state.is_logged_in === false){ + if(this.state.is_logged_in === false && currentShare() === null){ Session.currentUser().then((res) => { if(res.is_authenticated === false){ this.props.error({message: "Authentication Required"}); @@ -38,7 +38,7 @@ export function LoggedInOnly(WrappedComponent){ } render(){ - if(this.state.is_logged_in === true){ + if(this.state.is_logged_in === true || currentShare() !== null){ return ; } return null; @@ -65,7 +65,7 @@ export function ErrorPage(WrappedComponent){ const message = this.state.error.message || "There is nothing in here"; return (
    - + home diff --git a/client/helpers/cache.js b/client/helpers/cache.js index f4fc36a8..bb363d1d 100644 --- a/client/helpers/cache.js +++ b/client/helpers/cache.js @@ -7,7 +7,7 @@ function Data(){ this._init(); } -const DB_VERSION = 2; +const DB_VERSION = 3; Data.prototype._init = function(){ const request = indexedDB.open('nuage', DB_VERSION); @@ -27,30 +27,34 @@ Data.prototype._setup = function(e){ let store; 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 // to make sure everything will be fine db.deleteObjectStore(this.FILE_PATH); 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.createIndex("idx_path", "path", { unique: true }); + store = db.createObjectStore(this.FILE_PATH, {keyPath: ["share", "path"]}); + store.createIndex("idx_path", ["share", "path"], { unique: true }); - store = db.createObjectStore(this.FILE_CONTENT, {keyPath: "path"}); - store.createIndex("idx_path", "path", { unique: true }); + store = db.createObjectStore(this.FILE_CONTENT, {keyPath: ["share", "path"]}); + store.createIndex("idx_path", ["share", "path"], { unique: true }); } /* * 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({}); return this.db.then((db) => { const tx = db.transaction(type, "readonly"); const store = tx.objectStore(type); - const query = store.get(path); + const query = store.get(key); return new Promise((done, error) => { query.onsuccess = (e) => { let data = query.result; @@ -61,13 +65,13 @@ Data.prototype.get = function(type, path){ }).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) => { const tx = db.transaction(type, "readwrite"); const store = tx.objectStore(type); - const range = exact === true? IDBKeyRange.only(path) : IDBKeyRange.bound( - path, - path+'\uFFFF', + const range = exact === true? IDBKeyRange.only(key) : IDBKeyRange.bound( + [key[0], key[1]], + [key[0], key[1]+'\uFFFF'], false, true ); const request = store.openCursor(range); @@ -77,7 +81,7 @@ Data.prototype.update = function(type, path, fn, exact = true){ const cursor = event.target.result; if(!cursor) return done(new_data); new_data = fn(cursor.value || null); - cursor.delete(cursor.value.path); + cursor.delete([key[0], cursor.value.path]); store.put(new_data); 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) => { const tx = db.transaction(type, "readwrite"); const store = tx.objectStore(type); - const query = store.get(path); + const query = store.get(key); return new Promise((done, error) => { query.onsuccess = (e) => { const new_data = fn(query.result || null); @@ -105,7 +109,7 @@ Data.prototype.upsert = function(type, path, fn){ }).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({}); return this.db.then((db) => { @@ -119,28 +123,28 @@ Data.prototype.add = function(type, path, data){ }).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) => { const tx = db.transaction(type, "readwrite"); const store = tx.objectStore(type); if(exact === true){ - const req = store.delete(path); + const req = store.delete(key); return new Promise((done, err) => { req.onsuccess = () => done(); req.onerror = err; }); }else{ const request = store.openCursor(IDBKeyRange.bound( - path, - path+'\uFFFF', + [key[0], key[1]], + [key[0], key[1]+'\uFFFF'], true, true )); return new Promise((done, err) => { request.onsuccess = function(event) { const cursor = event.target.result; if(cursor){ - cursor.delete(cursor.value.path); + cursor.delete([key[0], cursor.value.path]); cursor.continue(); }else{ done(); @@ -151,12 +155,15 @@ Data.prototype.remove = function(type, path, exact = true){ }).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) => { const tx = db.transaction([type], "readonly"); const store = tx.objectStore(type); 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) => { request.onsuccess = function(event) { diff --git a/client/helpers/common.js b/client/helpers/common.js index ab39f90a..b5735bd8 100644 --- a/client/helpers/common.js +++ b/client/helpers/common.js @@ -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; 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(); +} diff --git a/client/helpers/index.js b/client/helpers/index.js index 08b3cd84..1a04c97c 100644 --- a/client/helpers/index.js +++ b/client/helpers/index.js @@ -4,13 +4,13 @@ export { debounce, throttle } from './backpressure'; export { encrypt, decrypt } from './crypto'; export { event } from './events'; 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 { prepare } from './navigate'; export { invalidate, http_get, http_post, http_delete } from './ajax'; export { prompt, alert, confirm } from './popup'; export { notify } from './notify'; export { gid, randomString } from './random'; -export { leftPad } from './common'; +export { leftPad, copyToClipboard } from './common'; export { getMimeType } from './mimetype'; export { settings_get, settings_put } from './settings'; diff --git a/client/helpers/path.js b/client/helpers/path.js index 974669bf..10b7760e 100644 --- a/client/helpers/path.js +++ b/client/helpers/path.js @@ -35,3 +35,19 @@ export function absoluteToRelative(from, to){ } 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; +} diff --git a/client/model/files.js b/client/model/files.js index ec6ad0ff..b59d8807 100644 --- a/client/model/files.js +++ b/client/model/files.js @@ -1,7 +1,7 @@ "use strict"; 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 { cache } from '../helpers/'; @@ -46,16 +46,17 @@ class FileSystem{ } _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 cache.upsert(cache.FILE_PATH, path, (_files) => { + return cache.upsert(cache.FILE_PATH, [currentShare(), path], (_files) => { let store = Object.assign({ + share: currentShare(), path: path, results: null, access_count: 0, metadata: null }, _files); - store.access_count += 1; store.results = response.results || []; store.results = store.results.map((f) => { f.path = pathBuilder(path, f.name, f.type); @@ -64,6 +65,8 @@ class FileSystem{ store.metadata = response.metadata; if(_files && _files.results){ + store.access_count = _files.access_count; + // find out which entry we want to keep from the cache let _files_virtual_to_keep = _files.results.filter((file) => { return file.icon === 'loading'; @@ -101,44 +104,47 @@ class FileSystem{ } _ls_from_cache(path, _record_access = false){ - if(_record_access === false){ - return cache.get(cache.FILE_PATH, path).then((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 + return cache.get(cache.FILE_PATH, [currentShare(), path]).then((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 + }); + } + 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 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; - }); - } + return Promise.resolve(e); + }); } rm(path){ - const url = '/api/files/rm?path='+prepare(path); + const url = appendShareToUrl('/api/files/rm?path='+prepare(path)); return this._replace(path, 'loading') .then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res)) .then(() => http_get(url)) .then((res) => { - return cache.remove(cache.FILE_CONTENT, path) - .then(cache.remove(cache.FILE_CONTENT, path, false)) - .then(cache.remove(cache.FILE_PATH, dirname(path), false)) + return cache.remove(cache.FILE_CONTENT, [currentShare(), path]) + .then(cache.remove(cache.FILE_CONTENT, [currentShare(), path], false)) + .then(cache.remove(cache.FILE_PATH, [currentShare(), dirname(path)], false)) .then(this._remove(path, 'loading')) .then((res) => this.current_path === dirname(path) ? this._ls_from_cache(dirname(path)) : Promise.resolve(res)) }) @@ -150,14 +156,15 @@ class FileSystem{ } cat(path){ - const url = '/api/files/cat?path='+prepare(path); + const url = appendShareToUrl('/api/files/cat?path='+prepare(path)); return http_get(url, 'raw') .then((res) => { if(this.is_binary(res) === true){ 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 : { + share: currentShare(), path: path, last_update: null, last_access: null, @@ -173,7 +180,7 @@ class FileSystem{ .catch((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.access_count += 1; return response; @@ -184,12 +191,12 @@ class FileSystem{ }); } url(path){ - const url = '/api/files/cat?path='+prepare(path); + const url = appendShareToUrl('/api/files/cat?path='+prepare(path)); return Promise.resolve(url); } 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(); formData.append('file', file, "test"); return this._replace(path, 'loading') @@ -207,7 +214,7 @@ class FileSystem{ } 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'), destination_path = path; @@ -240,8 +247,9 @@ class FileSystem{ .then(() => { return this._replace(destination_path, null, 'loading') .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, + share: currentShare(), results: [], access_count: 0, last_access: null, @@ -310,12 +318,12 @@ class FileSystem{ function query(){ if(file){ - const url = '/api/files/cat?path='+prepare(path); + const url = appendShareToUrl('/api/files/cat?path='+prepare(path)); let formData = new window.FormData(); formData.append('file', file); return http_post(url, formData, 'multipart'); }else{ - const url = '/api/files/touch?path='+prepare(path); + const url = appendShareToUrl('/api/files/touch?path='+prepare(path)); return http_get(url); } } @@ -331,7 +339,7 @@ class FileSystem{ } 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, destination_path = to; @@ -344,11 +352,11 @@ class FileSystem{ .then(() => this._replace(destination_path, null, 'loading')) .then(() => this._refresh(origin_path, destination_path)) .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); return data; }, 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); return data; }, false); @@ -369,7 +377,7 @@ class FileSystem{ if(value.access_count >= 1 && value.path !== "/"){ data.push(value); } - }).then(() => { + }, cache.FILE_PATH, [currentShare(), "/"]).then(() => { return Promise.resolve( data .sort((a,b) => a.access_count > b.access_count? -1 : 1) @@ -389,8 +397,9 @@ class FileSystem{ }); function update_cache(result){ - return cache.upsert(cache.FILE_CONTENT, path, (response) => { + return cache.upsert(cache.FILE_CONTENT, [currentShare(), path], (response) => { if(!response) response = { + share: currentShare(), path: path, last_access: null, last_update: null, @@ -413,7 +422,7 @@ class FileSystem{ } _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) => { if(file.name === basename(path) && file.icon == icon_previous){ if(!icon){ delete file.icon; } @@ -425,7 +434,7 @@ class FileSystem{ }); } _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){ res = { path: dirname(path), @@ -445,7 +454,7 @@ class FileSystem{ }); } _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; res.results = res.results.filter((file) => { return file.name === basename(path) && file.icon == previous_icon ? false : true; @@ -461,5 +470,4 @@ class FileSystem{ } } - export const Files = new FileSystem(); diff --git a/client/model/share.js b/client/model/share.js index 68108a55..6a3c727c 100644 --- a/client/model/share.js +++ b/client/model/share.js @@ -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 { constructor(){} @@ -25,19 +25,20 @@ class ShareModel { } upsert(obj){ - const url = `/api/share/${obj.id}` + const url = appendShareToUrl(`/api/share/${obj.id}`) const data = Object.assign({}, obj); delete data.role; return http_post(url, data); } remove(id){ - const url = `/api/share/${id}`; + const url = appendShareToUrl(`/api/share/${id}`); return http_delete(url); } proof(id, data){ - // TODO + const url = `/api/share/${id}/proof`; + return http_post(url, data).then((res) => res.result); } } diff --git a/client/pages/error.scss b/client/pages/error.scss index 18a7b582..26a4bbb7 100644 --- a/client/pages/error.scss +++ b/client/pages/error.scss @@ -5,15 +5,17 @@ flex-direction: column; 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;} a{border-bottom: 1px dashed;} } .backnav { + font-weight: 100; .component_icon { - height: 25px; - margin-right: -2px; + height: 23px; + margin-right: -3px; + vertical-align: middle; } line-height: 25px; } diff --git a/client/pages/filespage.helper.js b/client/pages/filespage.helper.js index 96e35946..0e90c7de 100644 --- a/client/pages/filespage.helper.js +++ b/client/pages/filespage.helper.js @@ -1,7 +1,7 @@ import React from 'react'; import { Files } from '../model/'; -import { notify, alert } from '../helpers/'; +import { notify, alert, currentShare } from '../helpers/'; import Path from 'path'; import Worker from "../worker/search.worker.js"; 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 = "/") => { worker.postMessage({ action: "search::find", path: path, + share: currentShare(), keyword: keyword }); return new Observable((obs) => { diff --git a/client/pages/filespage.js b/client/pages/filespage.js index 8178595d..f2763cf0 100644 --- a/client/pages/filespage.js +++ b/client/pages/filespage.js @@ -244,6 +244,9 @@ export class FilesPage extends React.Component {
    +
    + THIS IS A MENUBAR +
    ); diff --git a/client/pages/filespage.scss b/client/pages/filespage.scss index 1c755186..c9f58d90 100644 --- a/client/pages/filespage.scss +++ b/client/pages/filespage.scss @@ -25,6 +25,13 @@ } } } + + .sidebar{ + width: 250px; + transition: width 0.3s ease; + background: var(--light); + &.close{width: 0;} + } } .scroll-y{ diff --git a/client/pages/filespage/frequently_access.js b/client/pages/filespage/frequently_access.js index 1f60fbb1..07935c4a 100644 --- a/client/pages/filespage/frequently_access.js +++ b/client/pages/filespage/frequently_access.js @@ -17,11 +17,12 @@ export class FrequentlyAccess extends React.Component { return ( + Quick Access
    { this.props.files.map(function(path, index){ return ( - +
    {Path.basename(path)}
    diff --git a/client/pages/filespage/frequently_access.scss b/client/pages/filespage/frequently_access.scss index 653bf475..274380d0 100644 --- a/client/pages/filespage/frequently_access.scss +++ b/client/pages/filespage/frequently_access.scss @@ -2,7 +2,7 @@ display: flex; a{ 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; overflow: hidden; margin-right: 5px; diff --git a/client/pages/filespage/share.js b/client/pages/filespage/share.js index 888ffe25..209b04f3 100644 --- a/client/pages/filespage/share.js +++ b/client/pages/filespage/share.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { NgIf, Icon } from '../../components/'; import { Share } from '../../model/'; -import { randomString, notify, absoluteToRelative } from '../../helpers/'; +import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/'; import './share.scss'; export class ShareComponent extends React.Component { @@ -32,14 +32,20 @@ export class ShareComponent extends React.Component { } componentDidMount(){ - Share.all(this.props.path) - .then((existings) => { - this.refreshModal(); - this.setState({existings: existings}); + Share.all(this.props.path).then((existings) => { + this.refreshModal(); + this.setState({ + existings: existings.sort((a, b) => { + return a.path.split("/").length > b.path.split("/").length; + }) }); + }); } updateState(key, value){ + if(key === "role"){ + this.setState(this.resetState()); + } if(this.state[key] === value){ this.setState({[key]: null}); }else{ @@ -85,10 +91,13 @@ export class ShareComponent extends React.Component { }); } - onRegisterLink(e){ - this.refs.$input.select(); - document.execCommand("copy"); + copyLinkInClipboard(link){ + copyToClipboard(link); notify.send("The link was copied in the clipboard", "INFO"); + } + + onRegisterLink(e){ + this.copyLinkInClipboard(this.refs.$input.value); const link = { role: this.state.role, @@ -137,9 +146,9 @@ export class ShareComponent extends React.Component { return Share.upsert(link) .then(() => { 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())) .catch((err) => { @@ -152,11 +161,13 @@ export class ShareComponent extends React.Component { render(){ const beautifulPath = function(from, to){ - return to; - const p = absoluteToRelative(from, to); - if(p === "./"){ - return "Current folder"; + if(filetype(from) === "directory"){ + from = from.split("/"); + from = from.slice(0, from.length - 1); + from = from.join("/"); } + + let p = absoluteToRelative(from, to); return p.length < to.length ? p : to; }; const urlify = function(str){ @@ -197,8 +208,10 @@ export class ShareComponent extends React.Component { this.state.existings && this.state.existings.map((link, i) => { return (
    - {link.role} - {beautifulPath(this.props.path, link.path)} + + {link.role} + + {beautifulPath(this.props.path, link.path)}
    @@ -221,16 +234,18 @@ export class ShareComponent extends React.Component {
    - + + + this.updateState('url', urlify(val))} inputType="text"/>
    - {}}/> + {}}/>
    @@ -240,15 +255,18 @@ export class ShareComponent extends React.Component { const SuperCheckbox = (props) => { const onCheckboxTick = (e) => { + if(props.inputType === undefined){ + return props.onChange(e.target.checked ? true : false); + } return props.onChange(e.target.checked ? "" : null); }; const onValueChange = (e) => { props.onChange(e.target.value); }; - const _is_expended = function(val){ - return val === null || val === undefined ? false : true; + return val === null || val === undefined || val === false ? false : true; }(props.value); + return (
    diff --git a/client/pages/sharepage.js b/client/pages/sharepage.js index 27dc3cca..6c4386ca 100644 --- a/client/pages/sharepage.js +++ b/client/pages/sharepage.js @@ -2,40 +2,60 @@ import React from 'react'; import { Redirect } from 'react-router'; import { Share } from '../model/'; -import { notify } from '../helpers/'; -import { Loader, Input, Button, Container } from '../components/'; +import { notify, basename, filetype } from '../helpers/'; +import { Loader, Input, Button, Container, ErrorPage, Icon, NgIf } from '../components/'; import './error.scss'; +import './sharepage.scss'; +@ErrorPage export class SharePage extends React.Component { constructor(props){ super(props); this.state = { - redirection: null, - loading: true, - request_password: false, - request_username: false + path: null, + key: null, + error: null, + loading: false }; } componentDidMount(){ - Share.get(this.props.match.params.id) - .then((res) => { - console.log(res); - this.setState({ - loading: false, - request_password: true - }); - }) - .catch((res) => { - this.setState({ - loading: false - }); - }); + this._proofQuery(this.props.match.params.id).then(() => { + if(this.refs.$input) { + this.refs.$input.ref.focus(); + } + }); } submitProof(e, type, value){ 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() { @@ -45,52 +65,60 @@ export class SharePage extends React.Component { }; }; - if(this.state.loading === true){ - return (
    ); - } + let className = this.state.error ? "error rand-"+Math.random().toString() : ""; - if(this.state.request_password === true){ - return ( - -
    this.submitProof(e, "password", this.refs.$input.ref.value)} style={marginTop()}> - - -
    -
    - ); - }else if(this.state.request_username === true){ - return ( - -
    this.submitProof(e, "email", this.refs.$input.ref.value)} style={marginTop()}> - - -
    -
    - ); - }else if(this.state.request_verification === true){ - return ( - -
    this.submitProof(e, "code", this.refs.$input.ref.value)} style={marginTop()}> - - -
    -
    - ); - } - - if(this.state.redirection !== null){ - if(this.state.redirection.slice(-1) === "/"){ - return ( ); + if(this.state.path !== null){ + if(filetype(this.state.path) === "directory"){ + return ( ); }else{ - return ( ); + return ( ); } - }else{ + } else if (this.state.key === null){ return ( -
    -

    Oops!

    -

    There's nothing in here

    +
    +
    ); + } else if(this.state.key === "code"){ + return ( + +
    this.submitProof(e, "code", this.refs.$input.ref.value)} style={marginTop()}> + + +
    +
    + ); + } else if(this.state.key === "password"){ + return ( + +
    this.submitProof(e, "password", this.refs.$input.ref.value)} style={marginTop()}> + + +
    +
    + ); + }else if(this.state.key === "email"){ + return ( + +
    this.submitProof(e, "email", this.refs.$input.ref.value)} style={marginTop()}> + + +
    +
    + ); } + + return ( +
    +

    Oops!

    +

    There's nothing in here

    +
    + ); } } diff --git a/client/pages/sharepage.scss b/client/pages/sharepage.scss new file mode 100644 index 00000000..5445ebcb --- /dev/null +++ b/client/pages/sharepage.scss @@ -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); + } +} diff --git a/client/pages/viewerpage/org_viewer.js b/client/pages/viewerpage/org_viewer.js index b98a605c..ea38537f 100644 --- a/client/pages/viewerpage/org_viewer.js +++ b/client/pages/viewerpage/org_viewer.js @@ -309,7 +309,7 @@ class OrgViewer extends React.Component {
    ); }) - } + }
    diff --git a/client/pages/viewerpage/pager.js b/client/pages/viewerpage/pager.js index 0d8433a2..f7634a57 100644 --- a/client/pages/viewerpage/pager.js +++ b/client/pages/viewerpage/pager.js @@ -4,7 +4,7 @@ import { Link, withRouter } from 'react-router-dom'; import { Files } from '../../model/'; import { sort } from '../../pages/filespage.helper.js'; 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'; @@ -25,7 +25,6 @@ export class Pager extends React.Component { componentDidMount(){ this.setNavigation(this.props); window.addEventListener("keyup", this.navigate); - this.props.subscribe('media::next', () => { this.navigatePage(this.calculateNextPageNumber(this.state.n)); }); @@ -35,7 +34,7 @@ export class Pager extends React.Component { } componentWillReceiveProps(props){ - if(props.path === this.props.path){ + if(props.path !== this.props.path){ this.setNavigation(props); } } @@ -57,7 +56,8 @@ export class Pager extends React.Component { navigatePage(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(); 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){ @@ -79,6 +79,10 @@ export class Pager extends React.Component { setNavigation(props){ 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) => sort(f, settings_get('filespage_sort') || 'type')) .then((f) => findPosition(f, basename(props.path))) @@ -87,7 +91,8 @@ export class Pager extends React.Component { files: res[0], n: res[1] }); - }); + }) + .catch(() => {}); const findPosition = (files, filename) => { let i; diff --git a/client/worker/search.worker.js b/client/worker/search.worker.js index 71bc7297..a03b00a3 100644 --- a/client/worker/search.worker.js +++ b/client/worker/search.worker.js @@ -8,7 +8,7 @@ self.onmessage = function(message){ if(current_search != null){ 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}); }, null, () => { self.postMessage({type: "search::completed"}) @@ -16,7 +16,7 @@ self.onmessage = function(message){ } } -function Search(path, keyword){ +function Search(key, keyword){ let results = []; return new Observable((obs) => { obs.next(results); @@ -32,7 +32,7 @@ function Search(path, keyword){ results = results.concat(found); obs.next(results); } - }, cache.FILE_PATH, path).then(() => { + }, cache.FILE_PATH, [key[0], key[1]]).then(() => { obs.complete(results); }); }); diff --git a/config/config.json b/config/config.json index 2b3b96d6..38a1dfbc 100644 --- a/config/config.json +++ b/config/config.json @@ -15,6 +15,11 @@ "level": "INFO", "telemetry": true }, + "smtp": { + "addr": "smtp.gmail.com", + "username": "mickael.kerjean@gmail.com", + "password": "test" + }, "oauth": { "gdrive": { "client_id": "", diff --git a/server/common/config.go b/server/common/config.go index f0d7f338..4ff83637 100644 --- a/server/common/config.go +++ b/server/common/config.go @@ -36,6 +36,13 @@ type Config struct { Level string `json:"level"` Telemetry bool `json:"telemetry"` } `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 { Dropbox struct { ClientID string `json:"client_id"` diff --git a/server/common/constants.go b/server/common/constants.go index a86c0daa..1e10de82 100644 --- a/server/common/constants.go +++ b/server/common/constants.go @@ -1,6 +1,7 @@ package common const ( - COOKIE_NAME = "auth" + COOKIE_NAME_AUTH = "auth" + COOKIE_NAME_PROOF = "proof" COOKIE_PATH = "/api/" ) diff --git a/server/common/crypto.go b/server/common/crypto.go index a9e264c4..918478d5 100644 --- a/server/common/crypto.go +++ b/server/common/crypto.go @@ -1,58 +1,144 @@ package common import ( + "bytes" + "compress/zlib" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha1" "encoding/base32" "encoding/base64" - "encoding/json" "io" + "io/ioutil" + mathrand "math/rand" + "math/big" ) -func Encrypt(keystr string, text map[string]string) (string, error) { - key := []byte(keystr) - plaintext, err := json.Marshal(text) - if err != nil { - return "", NewError("json marshalling: "+err.Error(), 500) - } +var Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - block, err := aes.NewCipher(key) +func EncryptString(secret string, json string) (string, error) { + d, err := compress([]byte(json)) if err != nil { - return "", NewError("encryption issue (cipher): "+err.Error(), 500) + return "", err } - ciphertext := make([]byte, aes.BlockSize+len(plaintext)) - iv := ciphertext[:aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return "", NewError("encryption issue: "+err.Error(), 500) + d, err = encrypt([]byte(secret), d) + if err != nil { + return "", err } - stream := cipher.NewCFBEncrypter(block, iv) - stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext) - return base64.URLEncoding.EncodeToString(ciphertext), nil + return base64.URLEncoding.EncodeToString(d), nil } -func Decrypt(keystr string, cryptoText string) (map[string]string, error) { - var raw map[string]string - - key := []byte(keystr) - 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) +func DecryptString(secret string, data string) (string, error){ + d, err := base64.URLEncoding.DecodeString(data) + if err != nil { + return "", err } - - iv := ciphertext[:aes.BlockSize] - ciphertext = ciphertext[aes.BlockSize:] - stream := cipher.NewCFBDecrypter(block, iv) - stream.XORKeyStream(ciphertext, ciphertext) - - json.Unmarshal(ciphertext, &raw) - return raw, nil + d, err = decrypt([]byte(secret), d) + if err != nil { + return "", err + } + d, err = decompress(d) + if err != nil { + return "", err + } + 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 += "host =>" + params["host"] p += "hostname =>" + params["hostname"] @@ -63,7 +149,5 @@ func GenerateID(params map[string]string) string { p += "endpoint =>" + params["endpoint"] p += "bearer =>" + params["bearer"] p += "token =>" + params["token"] - hasher := sha1.New() - hasher.Write([]byte(p)) - return "sha1::" + base32.HexEncoding.EncodeToString(hasher.Sum(nil)) + return Hash(p) } diff --git a/server/common/crypto_test.go b/server/common/crypto_test.go index 9a935bd8..f3ea20f8 100644 --- a/server/common/crypto_test.go +++ b/server/common/crypto_test.go @@ -5,18 +5,18 @@ import ( "github.com/stretchr/testify/assert" ) -func TestEncryptSomething(t *testing.T) { +func TestEncryptString(t *testing.T) { key := "test|test|test|test|test" - - d := make(map[string]string) - d["foo"] = "bar" - - str, err := Encrypt(key, d) + text := "I'm some text" + a, err := EncryptString(key, text) 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.Equal(t, "bar", data["foo"]) + assert.Equal(t, b, text) + } func TestIDGeneration(t *testing.T) { @@ -32,3 +32,11 @@ func TestIDGeneration(t *testing.T) { assert.NotEqual(t, id1, id2) 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) +} diff --git a/server/common/types.go b/server/common/types.go index a25f6327..7f6b7925 100644 --- a/server/common/types.go +++ b/server/common/types.go @@ -56,5 +56,7 @@ type Metadata struct { CanRename *bool `json:"can_rename,omitempty"` CanMove *bool `json:"can_move,omitempty"` CanUpload *bool `json:"can_upload,omitempty"` + CanDelete *bool `json:"can_delete,omitempty"` + CanShare *bool `json:"can_share,omitempty"` Expire *time.Time `json:"-"` } diff --git a/server/common/utils.go b/server/common/utils.go index 56c56ca7..c024e135 100644 --- a/server/common/utils.go +++ b/server/common/utils.go @@ -1,19 +1,5 @@ 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 { return &t } @@ -35,13 +21,13 @@ func NewBoolFromInterface(val interface{}) bool { default: return false } } -func NewIntpFromInterface(val interface{}) *int { +func NewInt64pFromInterface(val interface{}) *int64 { switch val.(type) { - case int: - v := val.(int) + case int64: + v := val.(int64) return &v case float64: - v := int(val.(float64)) + v := int64(val.(float64)) return &v default: return nil } diff --git a/server/ctrl/files.go b/server/ctrl/files.go index 1d2efdbc..6faa4e1f 100644 --- a/server/ctrl/files.go +++ b/server/ctrl/files.go @@ -3,6 +3,7 @@ package ctrl import ( . "github.com/mickael-kerjean/nuage/server/common" "github.com/mickael-kerjean/nuage/server/services" + "github.com/mickael-kerjean/nuage/server/model" "io" "net/http" "path/filepath" @@ -18,6 +19,16 @@ type FileInfo struct { } 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")) if err != nil { SendErrorResult(res, err) @@ -30,7 +41,6 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) { return } - files := []FileInfo{} for _, entry := range entries { f := FileInfo{ Name: entry.Name(), @@ -48,10 +58,28 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) { files = append(files, f) } - var perms *Metadata + var perms *Metadata = &Metadata{} if obj, ok := ctx.Backend.(interface{ Meta(path string) *Metadata }); ok { 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) } @@ -62,6 +90,10 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) { MaxAge: -1, Path: "/", }) + if model.CanRead(&ctx) == false { + SendErrorResult(res, NewError("Permission denied", 403)) + return + } path, err := pathBuilder(ctx, req.URL.Query().Get("path")) 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) { + if model.CanEdit(&ctx) == false { + SendErrorResult(res, NewError("Permission denied", 403)) + return + } + path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { 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) { + if model.CanEdit(&ctx) == false { + SendErrorResult(res, NewError("Permission denied", 403)) + return + } + from, err := pathBuilder(ctx, req.URL.Query().Get("from")) if err != nil { 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) { + if model.CanEdit(&ctx) == false { + SendErrorResult(res, NewError("Permission denied", 403)) + return + } + path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { 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) { + if model.CanUpload(&ctx) == false { + SendErrorResult(res, NewError("Permission denied", 403)) + return + } + path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { 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) { + if model.CanUpload(&ctx) == false { + SendErrorResult(res, NewError("Permission denied", 403)) + return + } + path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { SendErrorResult(res, err) diff --git a/server/ctrl/session.go b/server/ctrl/session.go index e1591e05..20631b81 100644 --- a/server/ctrl/session.go +++ b/server/ctrl/session.go @@ -1,6 +1,7 @@ package ctrl import ( + "encoding/json" "github.com/mickael-kerjean/mux" . "github.com/mickael-kerjean/nuage/server/common" "github.com/mickael-kerjean/nuage/server/model" @@ -17,7 +18,7 @@ func SessionGet(ctx App, res http.ResponseWriter, req *http.Request) { r := Session { IsAuth: false, } - + if ctx.Backend == nil { SendSuccessResult(res, r) return @@ -63,13 +64,19 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) { 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 { SendErrorResult(res, NewError(err.Error(), 500)) return } cookie := http.Cookie{ - Name: COOKIE_NAME, + Name: COOKIE_NAME_AUTH, Value: obfuscate, MaxAge: 60 * 60 * 24 * 30, 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) { cookie := http.Cookie{ - Name: COOKIE_NAME, + Name: COOKIE_NAME_AUTH, Value: "", Path: COOKIE_PATH, MaxAge: -1, diff --git a/server/ctrl/share.go b/server/ctrl/share.go index 91fbe7a6..07d57e03 100644 --- a/server/ctrl/share.go +++ b/server/ctrl/share.go @@ -1,10 +1,13 @@ package ctrl import ( + "encoding/json" + "fmt" "github.com/mickael-kerjean/mux" . "github.com/mickael-kerjean/nuage/server/common" "github.com/mickael-kerjean/nuage/server/model" "net/http" + "strings" ) 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) 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) { + if model.CanShare(&ctx) == false { + SendErrorResult(res, NewError("No permission", 403)) + return + } + s := extractParams(req, &ctx) 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 { SendErrorResult(res, err) @@ -37,13 +92,91 @@ func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) { SendSuccessResult(res, nil) } -func ShareGiveProof(ctx App, res http.ResponseWriter, req *http.Request) { - // switch NewStringFromInterface(ctx.Body["type"]) { - // case "password": - // case "code": nil - // case "email": nil - // } - SendSuccessResult(res, false) +func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) { + var submittedProof model.Proof + var verifiedProof []model.Proof + var requiredProof []model.Proof + var remainingProof []model.Proof + var s model.Share + + // 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 != "" { + 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) { @@ -58,17 +191,18 @@ func ShareDelete(ctx App, res http.ResponseWriter, req *http.Request) { func extractParams(req *http.Request, ctx *App) model.Share { return model.Share{ - Id: NewStringFromInterface(mux.Vars(req)["id"]), - Backend: NewStringFromInterface(GenerateID(ctx.Session)), - Path: NewStringFromInterface(req.URL.Query().Get("path")), - Password: NewStringpFromInterface(ctx.Body["password"]), - Users: NewStringpFromInterface(ctx.Body["users"]), - Expire: NewIntpFromInterface(ctx.Body["expire"]), - Url: NewStringpFromInterface(ctx.Body["url"]), + Auth: "", + Id: NewStringFromInterface(mux.Vars(req)["id"]), + Backend: NewStringFromInterface(GenerateID(ctx.Session)), + Path: NewStringFromInterface(req.URL.Query().Get("path")), + Password: NewStringpFromInterface(ctx.Body["password"]), + Users: NewStringpFromInterface(ctx.Body["users"]), + Expire: NewInt64pFromInterface(ctx.Body["expire"]), + Url: NewStringpFromInterface(ctx.Body["url"]), CanManageOwn: NewBoolFromInterface(ctx.Body["can_manage_own"]), - CanShare: NewBoolFromInterface(ctx.Body["can_share"]), - CanRead: NewBoolFromInterface(ctx.Body["can_read"]), - CanWrite: NewBoolFromInterface(ctx.Body["can_write"]), - CanUpload: NewBoolFromInterface(ctx.Body["can_upload"]), + CanShare: NewBoolFromInterface(ctx.Body["can_share"]), + CanRead: NewBoolFromInterface(ctx.Body["can_read"]), + CanWrite: NewBoolFromInterface(ctx.Body["can_write"]), + CanUpload: NewBoolFromInterface(ctx.Body["can_upload"]), } } diff --git a/server/model/index.go b/server/model/index.go index 45a15f56..d5ea3a1b 100644 --- a/server/model/index.go +++ b/server/model/index.go @@ -6,6 +6,7 @@ import ( . "github.com/mickael-kerjean/nuage/server/common" "path/filepath" "os" + "time" ) var DB *sql.DB @@ -16,19 +17,33 @@ func init() { cachePath := filepath.Join(GetCurrentDir(), DBCachePath) os.MkdirAll(cachePath, os.ModePerm) var err error - DB, err = sql.Open("sqlite3", cachePath+"/db.sql?_fk=true") - if err != nil { + if DB, err = sql.Open("sqlite3", cachePath+"/db.sql?_fk=true"); err != nil { 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 err != nil { - return + 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 { + stmt.Exec() } - 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) } diff --git a/server/model/permissions.go b/server/model/permissions.go index c5967088..35f28fb8 100644 --- a/server/model/permissions.go +++ b/server/model/permissions.go @@ -1,9 +1,38 @@ 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 } -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 } diff --git a/server/model/share.go b/server/model/share.go index 942af532..1f04083c 100644 --- a/server/model/share.go +++ b/server/model/share.go @@ -2,21 +2,38 @@ package model import ( . "github.com/mickael-kerjean/nuage/server/common" + "bytes" "database/sql" "encoding/json" + "fmt" "github.com/mattn/go-sqlite3" "golang.org/x/crypto/bcrypt" + "log" + "net/http" + "net/smtp" + "html/template" + "strings" + "time" ) 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 { Id string `json:"id"` Backend string `json:"-"` + Auth string `json:"auth,omitempty"` Path string `json:"path"` Password *string `json:"password,omitempty"` Users *string `json:"users,omitempty"` - Expire *int `json:"expire,omitempty"` + Expire *int64 `json:"expire,omitempty"` Url *string `json:"url,omitempty"` CanShare bool `json:"can_share"` CanManageOwn bool `json:"can_manage_own"` @@ -25,10 +42,27 @@ type Share struct { 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) { p := Share{ s.Id, s.Backend, + "", s.Path, func(pass *string) *string{ if pass != nil { @@ -57,7 +91,7 @@ func(s *Share) UnmarshallJSON(b []byte) error { switch key { case "password": s.Password = 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 "can_share": s.CanShare = 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) { - 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 { return nil, err } - rows, err := stmt.Query(p.Backend) + rows, err := stmt.Query(p.Backend, p.Path) if err != nil { return nil, err } @@ -83,7 +117,7 @@ func ShareList(p *Share) ([]Share, error) { var a Share var params []byte rows.Scan(&a.Id, &a.Path, ¶ms) - json.Unmarshal(params, &a) + json.Unmarshal(params, &a) sharedFiles = append(sharedFiles, a) } rows.Close() @@ -91,26 +125,16 @@ func ShareList(p *Share) ([]Share, error) { } func ShareGet(p *Share) error { - if err := shareGet(p); err != nil { - 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 = ?") + stmt, err := DB.Prepare("SELECT id, related_path, params, auth FROM share WHERE id = ?") if err != nil { return err } defer stmt.Close() row := stmt.QueryRow(p.Id) 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 { - return NewError("No Result", 404) + return NewError("Not Found", 404) } return err } @@ -123,7 +147,7 @@ func ShareUpsert(p *Share) error { if *p.Password == PASSWORD_DUMMY { var copy Share copy.Id = p.Id - shareGet(©); + ShareGet(©); p.Password = copy.Password } else { 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 { return err } j, _ := json.Marshal(&struct { Password *string `json:"password,omitempty"` Users *string `json:"users,omitempty"` - Expire *int `json:"expire,omitempty"` + Expire *int64 `json:"expire,omitempty"` Url *string `json:"url,omitempty"` CanShare bool `json:"can_share"` CanManageOwn bool `json:"can_manage_own"` @@ -171,7 +195,7 @@ func ShareUpsert(p *Share) error { CanWrite: p.CanWrite, 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 } @@ -183,3 +207,461 @@ func ShareDelete(p *Share) error { _, err = stmt.Exec(p.Id, p.Backend) 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 ` + + + + + + Nuage code + + + + + + + + + +
      +
    + + + Your code to login + + + + + + + + +
    + + + + +
    +

    Your verification code is: {{.Code}}

    +
    +
    + + + + + + +
    +
     
    + + +` +} diff --git a/server/ctrl/share_test.go b/server/model/share_test.go similarity index 55% rename from server/ctrl/share_test.go rename to server/model/share_test.go index 2f7e784b..e4b99e10 100644 --- a/server/ctrl/share_test.go +++ b/server/model/share_test.go @@ -1,13 +1,12 @@ -package ctrl +package model import ( "testing" - "github.com/mickael-kerjean/nuage/server/model" . "github.com/mickael-kerjean/nuage/server/common" "github.com/stretchr/testify/assert" ) -var shareObj = model.Share{ +var shareObj = Share{ Backend: "foo", Id: "foo", Path: "/var/www/", @@ -16,7 +15,7 @@ var shareObj = model.Share{ CanRead: true, CanManageOwn: true, CanShare: true, - Expire: NewInt(1537759505787), + Expire: NewInt64(1537759505787), } @@ -24,28 +23,28 @@ var shareObj = model.Share{ //// UPSERT func TestShareSimpleUpsert(t *testing.T) { - err := model.ShareUpsert(&shareObj); + err := ShareUpsert(&shareObj); assert.NoError(t, err) } func TestShareMultipleUpsert(t *testing.T) { - err := model.ShareUpsert(&shareObj); + err := ShareUpsert(&shareObj); assert.NoError(t, err) - err = model.ShareUpsert(&shareObj); + err = ShareUpsert(&shareObj); assert.NoError(t, err) - err = model.ShareGet(&shareObj) + err = ShareGet(&shareObj) assert.NoError(t, err) } func TestShareUpsertIsProperlyInserted(t *testing.T) { - err := model.ShareUpsert(&shareObj); + err := ShareUpsert(&shareObj); assert.NoError(t, err) - var obj model.Share + var obj Share obj.Id = "foo" - err = model.ShareGet(&obj) + err = ShareGet(&obj) assert.NoError(t, err) assert.NotNil(t, obj.Password) } @@ -54,28 +53,28 @@ func TestShareUpsertIsProperlyInserted(t *testing.T) { //// get func TestShareGetNonExisting(t *testing.T) { - var s model.Share = shareObj + var s Share = shareObj 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") } func TestShareGetExisting(t *testing.T) { - err := model.ShareUpsert(&shareObj); + err := ShareUpsert(&shareObj); assert.NoError(t, err, "Upsert issue") - err = model.ShareGet(&shareObj); + err = ShareGet(&shareObj); assert.NoError(t, err) } -func TestShareGetExistingMakeSureDataIsOk(t *testing.T) { - err := model.ShareUpsert(&shareObj); +func TestShareGetExistingMakeSureDataIsOk(t *testing.T) { + err := ShareUpsert(&shareObj); assert.NoError(t, err, "Upsert issue") - var obj model.Share + var obj Share obj.Id = "foo" obj.Backend = shareObj.Backend - err = model.ShareGet(&obj); + err = ShareGet(&obj); assert.NoError(t, err) assert.Equal(t, "foo", obj.Id) 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.CanUpload) assert.Equal(t, "foo", obj.Backend) - assert.Equal(t, shareObj.Expire, obj.Expire) - assert.Equal(t, "{{PASSWORD}}", *obj.Password) + assert.Equal(t, shareObj.Expire, obj.Expire) } ////////////////////////////////////////////// @@ -94,11 +92,11 @@ func TestShareGetExistingMakeSureDataIsOk(t *testing.T) { func TestShareListAll(t *testing.T) { // Initialise test - err := model.ShareUpsert(&shareObj); + err := ShareUpsert(&shareObj); assert.NoError(t, err, "Upsert issue") // Actual test - list, err := model.ShareList(&shareObj) + list, err := ShareList(&shareObj) assert.NoError(t, err) assert.Len(t, list, 1) assert.NotNil(t, list[0].Password) @@ -110,15 +108,49 @@ func TestShareListAll(t *testing.T) { func TestShareDeleteShares(t *testing.T) { // Initialise test - err := model.ShareUpsert(&shareObj); + err := ShareUpsert(&shareObj); assert.NoError(t, err, "Upsert issue") - err = model.ShareGet(&shareObj) - assert.NoError(t, err) - - // Actual Test - err = model.ShareDelete(&shareObj); + err = ShareGet(&shareObj) assert.NoError(t, err) - err = model.ShareGet(&shareObj) - assert.Error(t, err) + // Actual Test + 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) + } diff --git a/server/router/index.go b/server/router/index.go index 898ccbf1..fb1a942f 100644 --- a/server/router/index.go +++ b/server/router/index.go @@ -33,8 +33,8 @@ func Init(a *App) *http.Server { share.HandleFunc("/{id}", APIHandler(ShareGet, *a)).Methods("GET") share.HandleFunc("/{id}", APIHandler(ShareUpsert, *a)).Methods("POST") 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 r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a)).Methods("GET") r.PathPrefix("/assets").Handler(StaticHandler("./data/public/", *a)).Methods("GET") diff --git a/server/router/middleware.go b/server/router/middleware.go index 6f50efed..9a4d367b 100644 --- a/server/router/middleware.go +++ b/server/router/middleware.go @@ -17,6 +17,7 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http. start := time.Now() ctx.Body, _ = extractBody(req) ctx.Session, _ = extractSession(req, &ctx) + ctx.Backend, _ = extractBackend(req, &ctx) 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) { - cookie, err := req.Cookie(COOKIE_NAME) - if err != nil { - return make(map[string]string), err + var str string + var res map[string]string + + 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) { diff --git a/server/services/pipeline.go b/server/services/pipeline.go index a366b88c..3c8eeb1e 100644 --- a/server/services/pipeline.go +++ b/server/services/pipeline.go @@ -41,7 +41,7 @@ func ProcessFileBeforeSend(reader io.Reader, ctx *App, req *http.Request, res *h ///////////////////////// // Specify transformation transform := &images.Transform{ - Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + RandomString(10)), + Temporary: ctx.Helpers.AbsolutePath(ImageCachePath + "image_" + QuickString(10)), Size: 300, Crop: true, Quality: 50,