feature (admin): admin console

This commit is contained in:
Mickael KERJEAN
2018-12-07 12:54:28 +11:00
parent 1b591af5b3
commit ce6a228968
78 changed files with 2699 additions and 386 deletions

View File

@ -83,6 +83,7 @@ a {
}
select {
-webkit-appearance: none;
-moz-appearance: none;
}
@ -100,8 +101,7 @@ button::-moz-focus-inner {
border: 0;
}
input,
textarea {
input, textarea, select {
transition: border 0.2s;
outline: none;
}
@ -109,9 +109,9 @@ textarea {
input[type="checkbox"] {
position: relative;
top: 1px;
margin: 0;
padding: 0;
vertical-align: top;
margin-top: auto;
margin-bottom: auto;
}
.no-select {

View File

@ -14,13 +14,16 @@ export class ModalAlert extends Popup {
alert.subscribe((Component, okCallback) => {
this.setState({
appear: true,
value: Component
value: Component,
fn: okCallback
});
});
}
onSubmit(e){
this.setState({appear: false});
this.setState({appear: false}, () => {
requestAnimationFrame(() => this.state.fn())
});
}
modalContentBody(){

View File

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

View File

@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import './container.scss';
export class Container extends React.Component {

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { browserHistory } from 'react-router'
import { browserHistory, Redirect } from 'react-router';
import { Session } from '../model/';
import { Session, Admin } from '../model/';
import { Container, Loader, Icon } from '../components/';
import { memory, currentShare } from '../helpers/';
@ -47,7 +47,6 @@ export function LoggedInOnly(WrappedComponent){
};
}
export function ErrorPage(WrappedComponent){
return class extends React.Component {
constructor(props){
@ -89,3 +88,11 @@ export function ErrorPage(WrappedComponent){
}
};
}
export const LoadingPage = (props) => {
return (
<div style={{marginTop: parseInt(window.innerHeight / 3)+'px'}}>
<Loader />
</div>
);
};

View File

@ -0,0 +1,170 @@
import React from 'react';
import { Input, Select, Enabler } from './';
import { FormObjToJSON, bcrypt_password, format } from '../helpers/';
import "./formbuilder.scss";
export class FormBuilder extends React.Component {
constructor(props){
super(props);
}
section(struct, key, level = 0){
if(struct == null) struct = "";
const isALeaf = function(struct){
if("label" in struct && "type" in struct &&
"value" in struct && "default" in struct){
return true;
}
return false;
};
if(Array.isArray(struct)) return null;
else if(isALeaf(struct) === false){
if(level <= 1){
return (
<div className="formbuilder">
{
key ? <h2 className="no-select">{ format(key) }</h2> : ""
}
{
Object.keys(struct).map((key, index) => {
return (
<div key={key+"-"+index}>
{ this.section(struct[key], key, level + 1) }
</div>
);
})
}
</div>
);
}
return (
<div>
<fieldset>
<legend className="no-select">{ format(key) }</legend>
{
Object.keys(struct).map((key, index) => {
return (
<div key={key+"-"+index}>
{ this.section(struct[key], key, level + 1) }
</div>
);
})
}
</fieldset>
</div>
);
}
let id = {};
let target = [];
if(struct.id !== undefined){
id.id = this.props.idx === undefined ? struct.id : struct.id + "_" + this.props.idx;
}
if(struct.type === "enable"){
target = struct.target.map((target) => {
return this.props.idx === undefined ? target : target + "_" + this.props.idx;
});
}
const onChange = function(e, fn){
struct.value = e;
if(typeof fn === "function"){
fn(struct);
}
this.props.onChange.call(
this,
FormObjToJSON(this.props.form)
);
};
return ( <FormElement render={this.props.render} onChange={onChange.bind(this)} {...id} params={struct} target={target} name={ format(struct.label) } /> );
}
render(){
return this.section(this.props.form || {});
}
}
const FormElement = (props) => {
const id = props.id !== undefined ? {id: props.id} : {};
let struct = props.params;
let $input = ( <Input onChange={(e) => props.onChange(e.target.value)} {...id} name={props.name} type="text" defaultValue={struct.value} placeholder={struct.placeholder} /> );
switch(props.params["type"]){
case "text":
const onTextChange = (value) => {
if(value === ""){
value = null;
}
props.onChange(value);
};
$input = ( <Input onChange={(e) => onTextChange(e.target.value)} {...id} name={props.name} type="text" value={struct.value || ""} placeholder={struct.placeholder}/> );
break;
case "number":
const onNumberChange = (value) => {
value = value === "" ? null : parseInt(value);
props.onChange(value);
};
$input = ( <Input onChange={(e) => onNumberChange(e.target.value)} {...id} name={props.name} type="number" value={struct.value || ""} placeholder={struct.placeholder} /> );
break;
case "password":
const onPasswordChange = (value) => {
if(value === ""){
value = null;
}
props.onChange(value);
};
$input = ( <Input onChange={(e) => onPasswordChange(e.target.value)} {...id} name={props.name} type="password" value={struct.value || ""} placeholder={struct.placeholder} /> );
break;
case "bcrypt":
const onBcryptChange = (value) => {
if(value === ""){
return props.onChange(null);
}
bcrypt_password(value).then((hash) => {
props.onChange(hash);
});
};
$input = ( <Input onChange={(e) => onBcryptChange(e.target.value)} {...id} name={props.name} type="password" value={struct.value || ""} placeholder={struct.placeholder} /> );
break;
case "hidden":
$input = ( <Input name={props.name} type="hidden" defaultValue={struct.value} /> );
break;
case "boolean":
$input = ( <Input onChange={(e) => props.onChange(e.target.checked)} {...id} name={props.name} type="checkbox" checked={struct.value === null ? !!struct.default : struct.value} /> );
break;
case "select":
$input = ( <Select onChange={(e) => props.onChange(e.target.value)} {...id} name={props.name} choices={struct.options} value={struct.value === null ? struct.default : struct.value} placeholder={struct.placeholder} />);
break;
case "enable":
$input = ( <Enabler onChange={(e) => props.onChange(e.target.checked)} {...id} name={props.name} target={props.target} defaultValue={struct.value === null ? struct.default : struct.value} /> );
break;
case "image":
$input = ( <img {...id} src={props.value} /> );
break;
}
if(props.render){
return props.render($input, props, struct, props.onChange.bind(null, null));
}
return (
<label className={"no-select input_type_" + props.params["type"]}>
<div>
<span>
{ format(struct.label) }:
</span>
<div style={{width: '100%'}}>
{ $input }
</div>
</div>
<div>
<span className="nothing"></span>
<div style={{width: '100%'}}>
{ struct.description ? (<div className="description">{struct.description}</div>) : null }
</div>
</div>
</label>
);
};

View File

@ -0,0 +1,28 @@
.formbuilder{
input[type="checkbox"]{
top: 5px;
}
.description{
margin-top: -5px;
margin-bottom: 10px;
opacity: 0.25;
font-size: 0.95em;
}
input::placeholder{
opacity: 0.5;
}
label.input_type_hidden{
display: none;
}
fieldset{
legend{
text-transform: uppercase;
font-weight: 200;
font-size: 1em;
padding: 0 15px;
}
}
}

View File

@ -1,6 +1,6 @@
export { EventEmitter, EventReceiver } from './events';
export { BreadCrumb, PathElement } from './breadcrumb';
export { Input } from './input';
export { Input, Select, Enabler } from './input';
export { Textarea } from './textarea';
export { Button } from './button';
export { Container } from './container';
@ -19,7 +19,5 @@ export { Audio } from './audio';
export { Video } from './video';
export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown';
export { MapShot } from './mapshot';
export { LoggedInOnly, ErrorPage } from './decorator';
//export { Connect } from './connect';
// Those are commented because they will delivered as a separate chunk
// export { Editor } from './editor';
export { LoggedInOnly, ErrorPage, LoadingPage } from './decorator';
export { FormBuilder } from './formbuilder';

View File

@ -11,9 +11,9 @@ export class Input extends React.Component {
return (
<input
className="component_input"
onChange={this.props.onChange}
{...this.props}
ref={(comp) => { this.ref = comp; }}
/>
ref={(comp) => { this.ref = comp; }} />
);
}
}
@ -22,3 +22,67 @@ Input.propTypes = {
type: PropTypes.string,
placeholder: PropTypes.string
};
export const Select = (props) => {
const choices = props.choices || [];
const id = props.id ? {id: props.id} : {};
return (
<select className="component_select" {...id} name={props.name} onChange={props.onChange} defaultValue={props.value}>
<option hidden>{props.placeholder}</option>
{
choices.map((choice, index) => {
return (
<option key={index} name={choice}>{choice}</option>
);
})
}
</select>
);
};
export class Enabler extends React.Component {
constructor(props){
super(props);
}
componentWillMount(){
requestAnimationFrame(() => {
this.toggle(this.props.defaultValue || false);
});
}
onChange(e){
this.toggle(e.target.checked);
this.props.onChange(e);
}
toggle(value){
const target = this.props.target || [];
target.map((t) => {
let $el = document.getElementById(t);
if(!$el) return;
if(value === true){
$el.parentElement.parentElement.parentElement.style.display = "block";
$el.parentElement.parentElement.parentElement.style.opacity = "1";
} else {
$el.parentElement.parentElement.parentElement.style.display = "none";
$el.parentElement.parentElement.parentElement.style.opacity = "0";
// reset value
if($el.value){
$el.value = null;
let event = new Event('input', { bubbles: true});
event.simulated = true;
$el.dispatchEvent(event);
}
}
});
}
render(){
return (
<Input type="checkbox" onChange={this.onChange.bind(this)} defaultChecked={this.props.defaultValue} />
);
}
};

View File

@ -21,3 +21,13 @@
border-color: var(--emphasis-primary);
}
}
.component_select{
background: inherit;
border-radius: 0;
border: none;
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
color: inherit;
width: 100%;
font-size: 1em;
}

View File

@ -8,7 +8,7 @@ export function http_get(url, type = 'json'){
if(type === 'json'){
try{
let data = JSON.parse(xhr.responseText);
if(data.status === 'ok'){
if("status" in data === false || data.status === 'ok'){
done(data);
}else{
err(data);

View File

@ -1,4 +1,6 @@
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
const algorithm = 'aes-256-cbc';
export function encrypt(obj, key){
@ -11,3 +13,12 @@ export function decrypt(text, key){
var decipher = crypto.createDecipher(algorithm, key)
return JSON.parse(decipher.update(text,'base64','utf8') + decipher.final('utf8'));
}
export function bcrypt_password(password) {
return new Promise((done, error) => {
bcrypt.hash(password, 10, (err, hash) => {
if(err) return error(err)
done(hash);
})
});
}

16
client/helpers/form.js Normal file
View File

@ -0,0 +1,16 @@
export const FormObjToJSON = function(o, fn){
let obj = Object.assign({}, o);
Object.keys(obj).map((key) => {
let t = obj[key];
if("label" in t && "type" in t && "default" in t && "value" in t){
if(typeof fn === "function"){
fn(obj, key);
} else {
obj[key] = obj[key].value;
}
} else {
obj[key] = FormObjToJSON(obj[key], fn);
}
});
return obj
};

View File

@ -1,7 +1,7 @@
export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout } from './navigate';
export { opener } from './mimetype';
export { debounce, throttle } from './backpressure';
export { encrypt, decrypt } from './crypto';
export { encrypt, decrypt, bcrypt_password } from './crypto';
export { event } from './events';
export { cache } from './cache';
export { pathBuilder, basename, dirname, absoluteToRelative, filetype, currentShare, appendShareToUrl } from './path';
@ -14,3 +14,5 @@ export { gid, randomString } from './random';
export { leftPad, copyToClipboard } from './common';
export { getMimeType } from './mimetype';
export { settings_get, settings_put } from './settings';
export { FormObjToJSON } from './form';
export { format } from './text';

10
client/helpers/text.js Normal file
View File

@ -0,0 +1,10 @@
export function format(str = ""){
if(str.length === 0) return str;
return str.split("_")
.map((word, index) => {
if(index != 0) return word;
return word[0].toUpperCase() + word.substring(1);
})
.join(" ");
}

View File

@ -26,7 +26,6 @@
<link rel="icon" type="image/png" sizes="96x96" href="/assets/logo/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/logo/favicon-16x16.png">
<link rel="icon" href="/assets/logo/favicon.ico" type="image/x-icon" />
<script src="/api/config"></script>
<meta name="msapplication-TileColor" content="#f2f2f2">
<meta name="msapplication-TileImage" content="/assets/logo/ms-icon-144x144.png">
@ -35,6 +34,19 @@
<meta name="description" content="browse your files in the cloud">
</head>
<body>
<div id="main" style="height: 100%"></div>
<div id="main" style="height: 100%">
<style>
html{
background: #f2f3f5;
color: #375160;
}
</style>
<script>
if(location.pathname == "/" || location.pathname == "/login"){
$style = document.querySelector("style");
$style.innerText = $style.innerText.replace("#f2f3f5", "#9AD1ED")
}
</script>
</div>
</body>
</html>

View File

@ -2,13 +2,17 @@ import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router';
import { Config } from "./model/"
import './assets/css/reset.scss';
window.addEventListener("DOMContentLoaded", () => {
const className = 'ontouchstart' in window ? 'touch-yes' : 'touch-no';
document.body.classList.add(className);
ReactDOM.render(<Router/>, document.getElementById('main'));
Config.refresh().then(() => {
ReactDOM.render(<Router/>, document.getElementById('main'));
});
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/assets/worker/cache.js').catch(function(error) {

10
client/model/admin.js Normal file
View File

@ -0,0 +1,10 @@
import { http_post, http_get } from '../helpers';
export const Admin = {
login: function(password = ""){
return http_post("/admin/api/session", {password: password});
},
isAdmin: function(){
return http_get("/admin/api/session").then((res) => res.result);
}
};

61
client/model/config.js Normal file
View File

@ -0,0 +1,61 @@
import { http_get, http_post, http_delete, debounce } from '../helpers/';
class ConfigModel {
constructor(){}
all(){
return http_get("/admin/api/config").then((d) => d.result);
}
save(config, debounced = true, fn_ok, fn_err){
let url = "/admin/api/config";
if(debounced){
if(!this.debounced_post){
this.debounced_post = debounce((url, config) => {
return http_post(url, config).then(this.refresh).then((a) => {
if(typeof fn_ok === "function") return fn_ok();
return Promise.resolve(a)
}).catch((err) => {
if(typeof fn_err === "function") return fn_err();
return Promise.reject(err)
});
}, 1000);
}
return this.debounced_post(url, config)
}
return http_post(url, config).then(this.refresh).then((a) => {
if(typeof fn_ok === "function") return fn_ok();
return Promise.resolve(a)
}).catch((err) => {
if(typeof fn_err === "function") return fn_err();
return Promise.reject(err)
});
}
refresh(){
return http_get("/api/config").then((config) => {
window.CONFIG = config.result;
});
}
}
class PluginModel {
constructor(){}
all(){
return http_get("/admin/api/plugin").then((r) => r.results);
}
}
class BackendModel {
constructor(){}
all(){
return http_get("/admin/api/backend").then((r) => r.result);
}
}
export const Plugin = new PluginModel();
export const Config = new ConfigModel();
export const Backend = new BackendModel();

View File

@ -1,3 +1,6 @@
export { Files } from './files';
export { Session } from './session';
export { Share } from './share';
export { Files } from "./files";
export { Session } from "./session";
export { Share } from "./share";
export { Config, Plugin, Backend } from "./config";
export { Log } from "./log";
export { Admin } from "./admin"

19
client/model/log.js Normal file
View File

@ -0,0 +1,19 @@
import { http_get } from '../helpers/';
class LogManager{
constructor(){}
get(maxSize = -1){
let url = this.url();
if(maxSize > 0){
url += "?maxSize="+maxSize
}
return http_get(url, 'text');
}
url(){
return "/admin/api/log"
}
}
export const Log = new LogManager();

113
client/pages/adminpage.js Normal file
View File

@ -0,0 +1,113 @@
import React from 'react';
import Path from 'path';
import { Route, Switch, Link, NavLink } from 'react-router-dom';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import './error.scss';
import './adminpage.scss';
import { Icon, LoadingPage } from '../components/';
import { Config, Admin } from '../model';
import { notify } from '../helpers/';
import { HomePage, DashboardPage, ConfigPage, LogPage, PluginPage, SupportPage, SetupPage, LoginPage } from './adminpage/';
function AdminOnly(WrappedComponent){
return class extends React.Component {
constructor(props){
super(props);
this.state = {
isAdmin: null
};
this.admin = () => {
Admin.isAdmin().then((t) => {
this.setState({isAdmin: t});
}).catch((err) => {
notify.send("Error: " + (err && err.message) , "error");
});
};
}
componentWillMount(){
this.timeout = window.setInterval(this.admin.bind(this), 30 * 1000);
this.admin.call(this);
}
componentWillUnmount(){
window.clearInterval(this.timeout);
}
render(){
if(this.state.isAdmin === true){
return ( <WrappedComponent {...this.props} /> );
} else if(this.state.isAdmin === false) {
return ( <LoginPage reload={() => this.admin()} /> );
}
return ( <LoadingPage />);
}
};
}
@AdminOnly
export class AdminPage extends React.Component {
constructor(props){
super(props);
this.state = {
isAdmin: null
};
}
render(){
return (
<div className="component_page_admin">
<SideMenu url={this.props.match.url}/>
<div className="page_container scroll-y">
<ReactCSSTransitionGroup key={window.location.pathname} transitionName="adminpage" transitionLeave={true} transitionEnter={true} transitionLeaveTimeout={15000} transitionEnterTimeout={20000} transitionAppear={true} transitionAppearTimeout={20000}>
<Switch>
<Route path={this.props.match.url + "/dashboard"} component={DashboardPage} />
<Route path={this.props.match.url + "/configure"} component={ConfigPage} />
<Route path={this.props.match.url + "/activity"} component={LogPage} />
<Route path={this.props.match.url + "/plugins"} component={PluginPage} />
<Route path={this.props.match.url + "/support"} component={SupportPage} />
<Route path={this.props.match.url + "/setup"} component={SetupPage} />
<Route path={this.props.match.url} component={HomePage} />
</Switch>
</ReactCSSTransitionGroup>
</div>
</div>
);
}
}
const SideMenu = (props) => {
return (
<div className="component_menu_sidebar no-select">
<NavLink to="/" className="header">
<Icon name="arrow_left" />
<img src="/assets/logo/icon-192x192.png" />
</NavLink>
<h2>Admin console</h2>
<ul>
<li>
<NavLink activeClassName="active" to={props.url + "/dashboard"}>
Dashboard
</NavLink>
</li>
<li>
<NavLink activeClassName="active" to={props.url + "/configure"}>
Configure
</NavLink>
</li>
<li>
<NavLink activeClassName="active" to={props.url + "/activity"}>
Activity
</NavLink>
</li>
<li>
<NavLink activeClassName="active" to={props.url + "/support"}>
Support
</NavLink>
</li>
</ul>
</div>
);
};

165
client/pages/adminpage.scss Normal file
View File

@ -0,0 +1,165 @@
.component_page_admin{
display: flex;
.page_container{
width: 100%;
background: var(--super-light);
padding-bottom: 150px;
padding-left: 60px;
padding-right: 60px;
@media screen and (max-width: 1000px) { padding-left: 30px; padding-right: 30px; }
@media screen and (max-width: 500px) { padding-left: 10px; padding-right: 10px; }
box-sizing: border-box;
max-height: 100vh;
h2{
font-family: 'Source Code Pro', monospace;
text-shadow: 0 0 2px var(--bg-color);
font-size: 2.8em;
padding: 60px 0 0 0;
margin-bottom: 30px;
margin-top: 0;
@media screen and (max-width: 1000px) { padding: 25px 0 0 0; }
@media screen and (max-width: 500px) { padding: 10px 0 0 0; }
font-weight: 300;
line-height: 1em;
&:after{
content: "_";
display: block;
font-size: 0;
border-bottom: 3px solid var(--color);
width: 90px;
margin-top: 10px;
opacity: 0.9;
line-height: 0;
}
}
.sticky h2{
position: sticky;
background: var(--super-light);
z-index: 2;
top: 0;
}
label{
> div{
display: flex;
@media screen and (max-width: 550px) {
display: block;
.nothing{ display: none; }
}
> span{
display: inline-block;
line-height: 30px;
min-width: 150px;
@media screen and (max-width: 760px) { min-width: 115px }
padding-right: 20px;
margin-top: auto;
margin-bottom: auto;
}
}
}
a{
color: var(--dark);
border-bottom: 1px dashed var(--dark);
}
pre{
font-family: 'Source Code Pro', monospace;
background: var(--dark);
padding: 10px;
margin-bottom: 0;
border-radius: 2px;
color: white;
max-width: 100%;
overflow-x: auto;
overflow-y: auto;
}
.component_loader > svg{
height: 50px;
}
fieldset{
background: white;
border-color: var(--super-light);
border-radius: 3px;
margin: 15px 0;
}
}
}
.component_menu_sidebar{
height: 100vh;
background: var(--dark);
width: 250px;
border-right: 2px solid var(--color);
padding: 50px 0px 0px 40px;
transition: transform 0.3s ease;
transition-delay: 0.7s;
h2{
font-family: 'Source Code Pro', monospace;
color: var(--primary);
font-weight: 300;
font-size: 1.5em;
margin: 25px 0 40px 0;
}
ul {
color: var(--light);
list-style-type: none;
padding: 0;
li {
margin: 15px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
a.active, a:hover{
color: var(--primary);
}
}
}
.header{
img{
width: 40px;
height: 40px;
}
img[alt="arrow_left"]{
position: absolute;
margin-left: -35px;
opacity: 0.7;
padding: 8px;
box-sizing: border-box;
}
}
@media screen and (max-width: 1000px) { padding-left: 30px; width: 200px; }
@media screen and (max-width: 760px) {
padding: 10px;
h2{
margin: 15px 0 25px 0;
font-size: 1.25em;
padding: 0;
}
}
@media screen and (max-width: 650px) {
width: inherit;
padding-left: 20px;
padding-right: 20px;
h2{ display: none; }
}
@media screen and (max-width: 440px) {
padding-left: 5px;
padding-right: 5px;
ul li{
width: 50px;
}
}
}
.adminpage-appear{
transition: transform 0.3s ease, opacity 0.5s ease;
opacity: 0;
transform: translateX(5px);
}
.adminpage-appear.adminpage-appear-active{
opacity: 1;
transform: translateX(0px);
}

View File

@ -0,0 +1,47 @@
import React from 'react';
import { FormBuilder } from '../../components/';
import { Config } from '../../model/';
export class ConfigPage extends React.Component {
constructor(props){
super(props);
this.state = {
form: {}
};
}
componentWillMount(){
Config.all().then((log) => {
this.setState({form: log});
});
}
format(name){
if(typeof name !== "string"){
return "N/A";
}
return name
.split("_")
.map((word) => {
if(word.length < 1){
return word;
}
return word[0].toUpperCase() + word.substring(1);
})
.join(" ");
}
onChange(form){
form.connections = window.CONFIG.connections
Config.save(form);
this.setState({refresh: Math.random()});
}
render(){
return (
<form className="sticky">
<FormBuilder form={this.state.form} onChange={this.onChange.bind(this)} />
</form>
);
}
}

View File

@ -0,0 +1,197 @@
import React from 'react';
import { FormBuilder, Icon, Input } from "../../components/";
import { Backend, Config } from "../../model/";
import { FormObjToJSON, notify, format } from "../../helpers/";
import "./dashboard.scss";
export class DashboardPage extends React.Component {
constructor(props){
super(props);
this.state = {
backend_enabled: [],
backend_available: [],
config: null
};
}
componentWillMount(){
Promise.all([
Backend.all(),
Config.all()
]).then((data) => {
let [backend, config] = data;
this.setState({
backend_available: backend,
backend_enabled: window.CONFIG.connections.map((conn) => {
return createFormBackend(backend, conn);
}),
config: config
});
});
}
onChange(e){
this.setState({refresh: Math.random()}); // refresh the screen to refresh the mutation
// that have happenned down the stack
let json = FormObjToJSON(this.state.config);
json.connections = this.state.backend_enabled.map((backend) => {
let data = FormObjToJSON(backend, (obj, key) => {
if(obj[key].enabled === true){
obj[key] = obj[key].value || obj[key].default;
} else {
delete obj[key];
}
});
const key = Object.keys(data)[0];
return data[key];
});
// persist config object in the backend
Config.save(json, true, () => {
this.componentWillMount();
});
}
addBackend(backend_id){
this.setState({
backend_enabled: this.state.backend_enabled.concat(
createFormBackend(this.state.backend_available, {
type: backend_id,
label: backend_id.toUpperCase()
})
)
}, this.onChange.bind(this));
}
removeBackend(n){
this.setState({
backend_enabled: this.state.backend_enabled.filter((_, i) => i !== n)
}, this.onChange.bind(this));
}
render(){
const update = (value, struct) => {
struct.enabled = value;
this.setState({refresh: Math.random()});
if(value === false){
struct.value = null;
}
return;
};
const enable = (struct) => {
if(typeof struct.value === "string"){
struct.enabled = true;
return true;
}
return !!struct.enabled;
};
return (
<div className="component_dashboard">
<h2>Dashboard</h2>
<div className="box-element">
{
Object.keys(this.state.backend_available).map((backend_available, index) => {
return (
<div key={index} className="backend">
<div>
{backend_available}
<span className="no-select" onClick={this.addBackend.bind(this, backend_available)}>
+
</span>
</div>
</div>
);
})
}
</div>
<form>
{
this.state.backend_enabled.map((backend_enable, index) => {
return (
<div key={index}>
<div className="icons no-select" onClick={this.removeBackend.bind(this, index)}>
<Icon name="delete" />
</div>
<FormBuilder onChange={this.onChange.bind(this)} idx={index} key={index}
form={{"": backend_enable}}
render={ ($input, props, struct, onChange) => {
let $checkbox = (
<Input type="checkbox" style={{width: "inherit", marginRight: '6px', top: '6px'}}
checked={enable(struct)} onChange={(e) => onChange(update.bind(this, e.target.checked))}/>
);
if(struct.label === "label"){
$checkbox = null;
}
return (
<label className={"no-select input_type_" + props.params["type"]}>
<div>
<span>
{ $checkbox }
{ format(struct.label) }:
</span>
<div style={{width: '100%'}}>
{ $input }
</div>
</div>
<div>
<span className="nothing"></span>
<div style={{width: '100%'}}>
{
struct.description ? (<div className="description">{struct.description}</div>) : null
}
</div>
</div>
</label>
);
}} />
</div>
);
})
}
</form>
</div>
);
}
}
function createFormBackend(backend_available, backend_data){
let template = JSON.parse(JSON.stringify(backend_available[backend_data.type]));
for(let key in backend_data){
if(key in template){
template[key].value = backend_data[key];
template[key].enabled = true;
} else {
// create a form object if data isn't available in the template
let obj = {};
obj[key] = {
label: key,
type: "text",
value: null,
default: backend_data[key]
};
template = Object.assign(obj, template);
}
if(key === "label"){
template[key].placeholder = "Name as shown on the login screen.";
template[key].value = backend_data[key];
template[key].enabled = true;
} else if(key === "type"){
template[key].enabled = true;
} else if(key === "advanced"){
template[key].enabled = true;
}
}
let obj = {};
obj[backend_data.type] = template;
return obj;
}

View File

@ -0,0 +1,63 @@
.component_dashboard{
.box-element {
display: flex;
flex-wrap: wrap;
margin: -5px -5px 20px -5px;
.backend{
position: relative;
width: 20%;
@media (max-width: 1350px){width: 25%;}
@media (max-width: 900px){width: 33.33%;}
@media (max-width: 750px){width: 50%;}
span{
cursor: pointer;
position: absolute;
top: 0px;
right: 0px;
font-size: 1.5em;
font-family: monospace;
display: inline-block;
width: 25px;
line-height: 25px;
background: var(--bg-color);
border-radius: 50%;
box-shadow: 2px 2px 2px var(--light);
color: var(--color);
text-shadow: none;
}
> div {
box-shadow: 2px 2px 10px var(--emphasis-primary);
margin: 8px;
padding: 30px 0;
text-align: center;
background: var(--primary);
color: white;
text-shadow: 0px 0px 1px var(--color);
font-size: 1.1em;
text-transform: uppercase;
border-radius: 2px;
}
}
}
form{
> div{
position: relative;
> .icons{
position: absolute;
border-radius: 50%;
padding: 10px;
background: var(--light);
cursor: pointer;
right: -15px;
top: -5px;
box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px;
.component_icon{
height: 20px;
}
}
}
}
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
export class HomePage extends React.Component {
constructor(props){
super(props);
this.state = {
stage: "loading"
}
}
render(){
return ( <Redirect to="/admin/dashboard" /> );
}
}

View File

@ -0,0 +1,8 @@
export { LogPage } from "./logger";
export { HomePage } from "./home";
export { ConfigPage } from "./config";
export { PluginPage } from "./plugin";
export { SupportPage } from "./support";
export { DashboardPage } from "./dashboard";
export { SetupPage } from "./setup";
export { LoginPage } from "./loginpage";

View File

@ -0,0 +1,70 @@
import React from 'react';
import { FormBuilder, Loader, Button, Icon } from '../../components/';
import { Config, Log } from '../../model/';
import { FormObjToJSON, notify } from '../../helpers/';
import "./logger.scss";
export class LogPage extends React.Component {
constructor(props){
super(props);
this.state = {
form: {},
loading: false,
log: "",
config: {}
};
}
componentWillMount(){
Config.all().then((config) => {
this.setState({
form: {"":{"params":config["log"]}},
config: FormObjToJSON(config)
});
});
Log.get(1024*100).then((log) => { // get only the last 100kb of log
this.setState({log: log}, () => {
this.refs.$log.scrollTop = this.refs.$log.scrollHeight;
});
});
}
onChange(r){
this.state.config["log"] = r[""].params;
this.state.config["connections"] = window.CONFIG.connections;
this.setState({loading: true}, () => {
Config.save(this.state.config, false, () => {
this.setState({loading: false});
}, () => {
notify.send("Error while saving config", "error");
this.setState({loading: false});
});
});
}
render(){
const filename = () => {
let tmp = "access_";
tmp += new Date().toISOString().substring(0,10).replace(/-/g, "");
tmp += ".log";
}
return (
<div className="component_logpage">
<h2>Logging { this.state.loading === true ? <Icon style={{height: '40px'}} name="loading"/> : null}</h2>
<div style={{minHeight: '150px'}}>
<FormBuilder form={this.state.form} onChange={this.onChange.bind(this)} />
</div>
<pre style={{height: '350px'}} ref="$log">
{
this.state.log === "" ? <Loader/> : this.state.log + "\n\n\n\n\n"
}
</pre>
<div>
<a href={Log.url()} download={filename()}><Button className="primary">Download</Button></a>
</div>
</div>
);
}
}

View File

@ -0,0 +1,9 @@
.component_logpage{
button{
width: inherit;
float: right;
margin-top: 5px;
padding-left: 20px;
padding-right: 20px;
}
}

View File

@ -0,0 +1,53 @@
import React from 'react';
import { Redirect } from 'react-router';
import { Input, Button, Container, Icon, Loader } from '../../components/';
import { Config, Admin } from '../../model/';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
export class LoginPage extends React.Component {
constructor(props){
super(props);
this.state = {
loading: false,
error: null
};
}
componentDidMount(){
this.refs.$input.ref.focus();
}
authenticate(e){
e.preventDefault();
this.setState({loading: true});
Admin.login(this.refs.$input.ref.value)
.then(() => this.props.reload())
.catch(() => {
this.refs.$input.ref.value = "";
this.setState({
loading: false,
error: true
}, () => {
window.setTimeout(() => {
this.setState({error: false});
}, 500);
});
});
}
render(){
const marginTop = () => { return {marginTop: parseInt(window.innerHeight / 3)+'px'};};
return (
<Container maxWidth="300px" className="sharepage_component">
<form className={this.state.error ? "error" : ""} onSubmit={this.authenticate.bind(this)} style={marginTop()}>
<Input ref="$input" type="text" placeholder="Password" />
<Button theme="transparent">
<Icon name={this.state.loading ? "loading" : "arrow_right"}/>
</Button>
</form>
</Container>
);
}
}

View File

@ -0,0 +1,41 @@
import React from 'react';
import { Plugin } from '../../model/';
import './plugin.scss';
const PluginBox = (props) => {
return (
<div className="component_pluginbox">
<div className="title">{props.name}</div>
<div>{props.description}</div>
</div>
);
};
export class PluginPage extends React.Component {
constructor(props){
super(props);
this.state = {
plugins: []
};
}
componentWillMount(){
Plugin.all().then((list) => this.setState({plugins: list}));
}
render(){
return (
<div className="component_plugin">
<h2>Plugins</h2>
<div>
{
this.state.plugins.map((plugin, index) => {
return ( <PluginBox key={index} name={plugin.name} author={plugin.author} description={plugin.description} /> );
})
}
</div>
</div>
);
}
}

View File

@ -0,0 +1,21 @@
.component_pluginbox{
background: white;
padding: 10px;
box-shadow: 2px 2px 2px var(--bg-color);
margin-bottom: 10px;
.title{
color: var(--dark);
font-size: 1.2em;
margin-bottom: 5px;
}
}
.component_plugin{
input[type="file"]{
background: white;
padding: 20px;
border: 2px solid var(--bg-color);
width: 100%;
box-sizing: border-box;
}
}

View File

@ -0,0 +1,113 @@
import React from 'react';
import { Input, Button, Container, Icon } from '../../components/';
import { Config, Admin } from '../../model/';
import { notify, FormObjToJSON, alert, prompt, bcrypt_password } from '../../helpers';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import "./setup.scss";
export class SetupPage extends React.Component {
constructor(props){
super(props);
this.state = {
stage: 0,
password: "",
enable_telemetry: false,
creating_password: false
};
}
createPassword(e){
this.setState({creating_password: true});
e.preventDefault();
Config.all().then((config) => {
this.setState({enable_telemetry: config.log.telemetry.value}, () => {
if(this.state.enable_telemetry === true) return;
this.unlisten = this.props.history.listen((location, action) => {
alert.now((
<div>
<p style={{textAlign: 'justify'}}>
Help making this software better by sending crash reports and anonymous usage statistics
</p>
<form onSubmit={this.start.bind(this)} style={{fontSize: '0.9em', marginTop: '10px'}}>
<label>
<Input type="checkbox" style={{width: 'inherit', marginRight: '10px'}} onChange={(e) => this.enableLog(e.target.checked)} defaultChecked={this.state.enable_telemetry} />
I accept but the data is not to be share with any third party
</label>
</form>
</div>
), () => this.unlisten());
});
});
bcrypt_password(this.state.password)
.then((hash) => {
config.auth.admin.value = hash;
config = FormObjToJSON(config);
config.connections = window.CONFIG.connections;
Config.save(config, false)
.then(() => Admin.login(this.state.password))
.then(() => this.setState({stage: 1, creating_password: false}))
.catch((err) => {
notify.send(err && err.message, "error");
this.setState({creating_password: false});
});
})
.catch((err) => {
notify.send("Hash error: " + JSON.stringify(err), "error");
this.setState({creating_password: false});
});
});
}
enableLog(value){
Config.all().then((config) => {
config.log.telemetry.value = value;
config = FormObjToJSON(config);
config.connections = window.CONFIG.connections;
Config.save(config, false);
});
}
start(e){
e.preventDefault();
this.props.history.push("/");
}
renderStage(stage){
if(stage === 0){
return (
<div>
<h2>You made it chief! { this.state.creating_password === true ? <Icon style={{height: '40px'}} name="loading"/> : null}</h2>
<p>
Let's start by protecting the admin area with a password:
</p>
<form onSubmit={this.createPassword.bind(this)} style={{maxWidth: '350px'}}>
<Input type="password" placeholder="Create your admin password" defaultValue="" onChange={(e) => this.setState({password: e.target.value})} autoComplete="new-password"/>
<Button className="primary">Create Password</Button>
</form>
<style dangerouslySetInnerHTML={{__html: ".component_menu_sidebar{transform: translateX(-300px)}"}} />
</div>
);
}
return (
<div>
<h2>Welcome to the engine room</h2>
<p>
This is the place where you can configure filestash to your liking. Feel free to poke around. <br/>
You can come back by navigating at <a href="/admin">`{window.location.origin + "/admin"}`</a>. <br/>
Have fun!
</p>
</div>
);
}
render(){
return (
<div className="component_setup">
{ this.renderStage(this.state.stage) }
</div>
);
}
}

View File

@ -0,0 +1,17 @@
.component_setup{
transform: none!important; // transition and fixed posiiton doesn't cohabit well so we have to resort
// to remove animation on this page to preserve the layout
text-align: justify;
button.completed{
position: fixed;
bottom: 0;
right: 0;
width: 150px;
padding: 15px;
box-sizing: border-box;
border-top-left-radius: 10px;
font-size: 1.1em;
color: var(--emphasis);
}
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
export class SupportPage extends React.Component {
constructor(props){
super(props);
}
render(){
return (
<div>
<h2>Support</h2>
<p>
<a href="mailto:mickael@kerjean.me">contact us</a> directly if you have/want enterprise support
</p>
<p>
There's also a community chat on Freenode - #filestash (or click <a href="https://kiwiirc.com/nextclient/#irc://irc.freenode.net/#filestash?nick=guest??">here</a> if you're not an IRC guru).
</p>
<h2>Quick Links</h2>
<ul>
<li><a href="https://www.filestash.app/support#faq">FAQ</a></li>
<li><a href="https://www.filestash.app/docs">Documentation</a></li>
<li><a href="https://www.filestash.app/">Our website</a></li>
</ul>
</div>
);
}
}

View File

@ -48,7 +48,7 @@ export class ConnectPage extends React.Component {
.then(Session.currentUser)
.then((user) => {
let url = '/files/';
let path = user.home
let path = user.home;
if(path){
path = path.replace(/^\/?(.*?)\/?$/, "$1");
if(path !== ""){
@ -108,7 +108,7 @@ export class ConnectPage extends React.Component {
render() {
return (
<div className="component_page_connect">
<NgIf cond={CONFIG["fork_button"]}>
<NgIf cond={window.CONFIG["fork_button"]}>
<ForkMe repo="https://github.com/mickael-kerjean/nuage" />
</NgIf>
<Container maxWidth="565px">

View File

@ -47,10 +47,12 @@
.component_page_connection_form.form-appear{
opacity: 0;
transform: translateX(5px);
}
.component_page_connection_form.form-appear.form-appear-active{
opacity: 1;
transition: opacity 0.5s ease-out;
transform: translateX(0);
transition: transform 0.25s ease-out, opacity 0.5s ease-out;
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NgIf, Icon } from '../../components/';
import { NgIf, Icon, Button } from '../../components/';
import { Share } from '../../model/';
import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/';
import './share.scss';

View File

@ -82,7 +82,6 @@ export class NewThing extends React.Component {
onSearchChange(search){
this.setState({search_keyword: search});
console.log(search);
}
render(){

View File

@ -25,9 +25,7 @@ export class HomePage extends React.Component {
this.setState({redirection: "/login"});
}
})
.catch((err) => {
this.setState({redirection: "/login"});
});
.catch((err) => this.setState({redirection: "/login"}));
}
render() {
if(this.state.redirection !== null){

View File

@ -60,9 +60,6 @@ export class Pager extends React.Component {
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){
console.log("> ISSUE: ", this.state.files[preload_index]);
}
Files.url(this.state.files[preload_index].path)
.then((url) => this.props.emit("media::preload", url))
.catch(() => {});

View File

@ -1,8 +1,14 @@
import React from 'react';
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
import { NotFoundPage, ConnectPage, HomePage, SharePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
import { Bundle, URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
import { ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video } from './components/';
import { URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
import { Bundle, ModalPrompt, ModalAlert, ModalConfirm, Notification, Audio, Video } from './components/';
const AdminPage = (props) => (
<Bundle loader={import(/* webpackChunkName: "admin" */"./pages/adminpage")} symbol="AdminPage">
{(Comp) => <Comp {...props}/>}
</Bundle>
);
export default class AppRouter extends React.Component {
render() {
@ -16,6 +22,7 @@ export default class AppRouter extends React.Component {
<Route path="/files/:path*" component={FilesPage} />
<Route path="/view/:path*" component={ViewerPage} />
<Route path="/logout" component={LogoutPage} />
<Route path="/admin" component={AdminPage} />
<Route component={NotFoundPage} />
</Switch>
</BrowserRouter>

View File

@ -11,7 +11,9 @@
},
"author": "",
"license": "ISC",
"dependencies": {},
"dependencies": {
"bcryptjs": "^2.4.3"
},
"devDependencies": {
"babel-cli": "^6.11.4",
"babel-core": "^6.13.2",

View File

@ -1,7 +1,6 @@
package common
type App struct {
Config *Config
Backend IBackend
Body map[string]interface{}
Session map[string]string

View File

@ -36,6 +36,10 @@ func (d *Driver) Get(name string) IBackend {
return b
}
func (d *Driver) Drivers() map[string]IBackend {
return d.ds
}
type Nothing struct {}
func (b Nothing) Init(params map[string]string, app *App) (IBackend, error) {
@ -65,3 +69,7 @@ func (b Nothing) Touch(path string) error {
func (b Nothing) Save(path string, file io.Reader) error {
return NewError("", 401)
}
func (b Nothing) LoginForm() Form {
return Form{}
}

View File

@ -48,3 +48,27 @@ func NewAppCache(arg ...time.Duration) AppCache {
c.Cache = cache.New(retention*time.Minute, cleanup*time.Minute)
return c
}
// ============================================================================
type KeyValueStore struct {
cache map[string]interface{}
}
func NewKeyValueStore() KeyValueStore {
return KeyValueStore{ cache: make(map[string]interface{}) }
}
func (this KeyValueStore) Get(key string) interface{} {
return this.cache[key]
}
func (this *KeyValueStore) Set(key string, value interface{}) {
this.cache[key] = value
}
func (this *KeyValueStore) Clear() {
this.cache = make(map[string]interface{})
}

View File

@ -1,183 +1,322 @@
package common
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"fmt"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"io/ioutil"
"os"
"path/filepath"
"sync"
"os"
"strings"
)
var SECRET_KEY string
var configPath string = filepath.Join(GetCurrentDir(), CONFIG_PATH + "config.json")
var (
Config Configuration
configPath string = filepath.Join(GetCurrentDir(), CONFIG_PATH + "config.json")
SECRET_KEY string
)
type Configuration struct {
mu sync.Mutex
currentElement *FormElement
cache KeyValueStore
form []Form
conn []map[string]interface{}
}
type Form struct {
Title string
Form []Form
Elmnts []FormElement
}
type FormElement struct {
Id string `json:"id,omitempty"`
Name string `json:"label"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
Placeholder string `json:"placeholder,omitempty"`
Opts []string `json:"options,omitempty"`
Target []string `json:"target,omitempty"`
Enabled bool `json:"enabled"`
Default interface{} `json:"default"`
Value interface{} `json:"value"`
}
func init() {
c := NewConfig()
// Let's initialise all our json config stuff
// For some reasons the file will be written bottom up so we start from the end moving up to the top
Config = NewConfiguration()
Config.Load()
Config.Init()
}
// Connections
if c.Get("connections.0.type").Interface() == nil {
c.Get("connections.-1").Set(map[string]interface{}{"type": "webdav", "label": "Webdav"})
c.Get("connections.-1").Set(map[string]interface{}{"type": "ftp", "label": "FTP"})
c.Get("connections.-1").Set(map[string]interface{}{"type": "sftp", "label": "SFTP"})
c.Get("connections.-1").Set(map[string]interface{}{"type": "git", "label": "GIT"})
c.Get("connections.-1").Set(map[string]interface{}{"type": "s3", "label": "S3"})
c.Get("connections.-1").Set(map[string]interface{}{"type": "dropbox", "label": "Dropbox"})
c.Get("connections.-1").Set(map[string]interface{}{"type": "gdrive", "label": "Drive"})
func NewConfiguration() Configuration {
return Configuration{
mu: sync.Mutex{},
cache: NewKeyValueStore(),
form: []Form{
Form{
Title: "general",
Elmnts: []FormElement{
FormElement{Name: "name", Type: "text", Default: "Nuage", Description: "Name has shown in the UI", Placeholder: "Default: \"Filestash\""},
FormElement{Name: "port", Type: "number", Default: 8334, Description: "Port on which the application is available.", Placeholder: "Default: 8334"},
FormElement{Name: "host", Type: "text", Default: "https://demo.filestash.app", Description: "The URL that users will use", Placeholder: "Eg: \"https://demo.filestash.app\""},
FormElement{Name: "secret_key", Type: "password", Description: "The key that's used to encrypt and decrypt content. Update this settings will invalidate existing user sessions and shared links, use with caution!"},
FormElement{Name: "editor", Type: "select", Default: "emacs", Opts: []string{"base", "emacs", "vim"}, Description: "Keybinding to be use in the editor. Default: \"emacs\""},
FormElement{Name: "fork_button", Type: "boolean", Default: true, Description: "Display the fork button in the login screen"},
FormElement{Name: "display_hidden", Type: "boolean", Default: false, Description: "Should files starting with a dot be visible by default?"},
FormElement{Name: "auto_connect", Type: "boolean", Default: false, Description: "User don't have to click on the login button if an admin is prefilling a unique backend"},
FormElement{Name: "remember_me", Type: "boolean", Default: true, Description: "Visiblity of the remember me button on the login screen"},
},
},
Form{
Title: "features",
Form: []Form{
Form{
Title: "search",
Elmnts: []FormElement{
FormElement{Name: "enable", Type: "boolean", Default: true, Description: "Enable/Disable the search feature"},
},
},
Form{
Title: "share",
Elmnts: []FormElement{
FormElement{Name: "enable", Type: "boolean", Default: true, Description: "Enable/Disable the share feature"},
},
},
},
},
Form{
Title: "log",
Elmnts: []FormElement{
FormElement{Name: "enable", Type: "enable", Target: []string{"log_level"}, Default: true},
FormElement{Name: "level", Type: "select", Default: "INFO", Opts: []string{"DEBUG", "INFO", "WARNING", "ERROR"}, Id: "log_level", Description: "Default: \"INFO\". This setting determines the level of detail at which log events are written to the log file"},
FormElement{Name: "telemetry", Type: "boolean", Default: false, Description: "We won't share anything with any third party. This will only to be used to improve Filestash"},
},
},
Form{
Title: "email",
Elmnts: []FormElement{
FormElement{Name: "server", Type: "text", Default: "smtp.gmail.com", Description: "Address of the SMTP email server.", Placeholder: "Default: smtp.gmail.com"},
FormElement{Name: "port", Type: "number", Default: 587, Description: "Port of the SMTP email server. Eg: 587", Placeholder: "Default: 587"},
FormElement{Name: "username", Type: "text", Description: "The username for authenticating to the SMTP server.", Placeholder: "Eg: username@gmail.com"},
FormElement{Name: "password", Type: "password", Description: "The password associated with the SMTP username.", Placeholder: "Eg: Your google password"},
FormElement{Name: "from", Type: "text", Description: "Email address visible on sent messages.", Placeholder: "Eg: username@gmail.com"},
},
},
Form{
Title: "auth",
Elmnts: []FormElement{
FormElement{Name: "admin", Type: "bcrypt", Default: "", Description: "Password of the admin section."},
},
Form: []Form{
Form{
Title: "custom",
Elmnts: []FormElement{
FormElement{Name: "client_secret", Type: "password"},
FormElement{Name: "client_id", Type: "text"},
FormElement{Name: "sso_domain", Type: "text"},
},
},
},
},
},
conn: make([]map[string]interface{}, 0),
}
}
func (this Form) MarshalJSON() ([]byte, error) {
return []byte(this.toJSON(func(el FormElement) string {
a, e := json.Marshal(el)
if e != nil {
return ""
}
return string(a)
})), nil
}
func (this Form) toJSON(fn func(el FormElement) string) string {
formatKey := func(str string) string {
str = strings.ToLower(str)
return strings.Replace(str, " ", "_", -1)
}
ret := ""
if this.Title != "" {
ret = fmt.Sprintf("%s\"%s\":", ret, formatKey(this.Title))
}
for i := 0; i < len(this.Elmnts); i++ {
if i == 0 {
ret = fmt.Sprintf("%s{", ret)
}
ret = fmt.Sprintf("%s\"%s\":%s", ret, formatKey(this.Elmnts[i].Name), fn(this.Elmnts[i]))
if i == len(this.Elmnts) - 1 && len(this.Form) == 0 {
ret = fmt.Sprintf("%s}", ret)
}
if i != len(this.Elmnts) - 1 || len(this.Form) != 0 {
ret = fmt.Sprintf("%s,", ret)
}
}
// OAuth credentials
c.Get("oauth").Default("")
// Features
c.Get("features.share.enable").Default(true)
c.Get("features.search.enable").Default(true)
// Log
c.Get("log.telemetry").Default(true)
c.Get("log.level").Default("INFO")
c.Get("log.enable").Default(true)
// Email
c.Get("email.from").Default("username@gmail.com")
c.Get("email.password").Default("password")
c.Get("email.username").Default("username@gmail.com")
c.Get("email.port").Default(587)
c.Get("email.server").Default("smtp.gmail.com")
// General
c.Get("general.remember_me").Default(true)
c.Get("general.auto_connect").Default(false)
c.Get("general.display_hidden").Default(true)
c.Get("general.fork_button").Default(true)
c.Get("general.editor").Default("emacs")
if c.Get("general.secret_key").String() == "" {
c.Get("general.secret_key").Set(RandomString(16))
for i := 0; i < len(this.Form); i++ {
if i == 0 && len(this.Elmnts) == 0 {
ret = fmt.Sprintf("%s{", ret)
}
ret = ret + this.Form[i].toJSON(fn)
if i == len(this.Form) - 1 {
ret = fmt.Sprintf("%s}", ret)
}
if i != len(this.Form) - 1 {
ret = fmt.Sprintf("%s,", ret)
}
}
if len(this.Form) == 0 && len(this.Elmnts) == 0 {
ret = fmt.Sprintf("%s{}", ret)
}
return ret
}
type FormIterator struct {
Path string
*FormElement
}
func (this *Form) Iterator() []FormIterator {
slice := make([]FormIterator, 0)
for i, _ := range this.Elmnts {
slice = append(slice, FormIterator{
strings.ToLower(this.Title),
&this.Elmnts[i],
})
}
for _, node := range this.Form {
r := node.Iterator()
if this.Title != "" {
for i := range r {
r[i].Path = strings.ToLower(this.Title) + "." + r[i].Path
}
}
slice = append(r, slice...)
}
return slice
}
func (this *Configuration) Load() {
file, err := os.OpenFile(configPath, os.O_RDONLY, os.ModePerm)
if err != nil {
Log.Warning("Can't read from config file")
return
}
defer file.Close()
cFile, err := ioutil.ReadAll(file)
if err != nil {
Log.Warning("Can't parse config file")
return
}
this.conn = func(cFile []byte) []map[string]interface{} {
var d struct {
Connections []map[string]interface{} `json:"connections"`
}
json.Unmarshal(cFile, &d)
return d.Connections
}(cFile)
this.form = func(cFile []byte) []Form {
f := Form{Form: this.form}
for _, el := range f.Iterator() {
value := gjson.Get(string(cFile), el.Path + "." + el.Name).Value()
if value != nil {
el.Value = value
}
}
return this.form
}(cFile)
Log.SetVisibility(this.Get("log.level").String())
return
}
func (this *Configuration) Init() {
if this.Get("general.secret_key").String() == "" {
key := RandomString(16)
this.Get("general.secret_key").Set(key)
}
SECRET_KEY = c.Get("general.secret_key").String()
if env := os.Getenv("APPLICATION_URL"); env != "" {
c.Get("general.host").Set(env)
} else {
c.Get("general.host").Default("http://127.0.0.1:8334")
this.Get("general.host").Set(env).String()
}
c.Get("general.port").Default(8334)
c.Get("general.name").Default("Nuage")
}
func NewConfig() *Config {
a := Config{}
return a.load()
}
type Config struct {
mu sync.Mutex
path *string
json string
reader gjson.Result
}
func (this *Config) load() *Config {
if f, err := os.OpenFile(configPath, os.O_RDONLY, os.ModePerm); err == nil {
j, _ := ioutil.ReadAll(f)
this.json = string(j)
f.Close()
} else {
this.json = `{}`
if len(this.conn) == 0 {
this.conn = []map[string]interface{}{
map[string]interface{}{
"type": "webdav",
"label": "WebDav",
},
map[string]interface{}{
"type": "ftp",
"label": "FTP",
},
map[string]interface{}{
"type": "sftp",
"label": "SFTP",
},
map[string]interface{}{
"type": "git",
"label": "GIT",
},
map[string]interface{}{
"type": "s3",
"label": "S3",
},
map[string]interface{}{
"type": "dropbox",
"label": "Dropbox",
},
map[string]interface{}{
"type": "gdrive",
"label": "Drive",
},
}
this.Save()
}
if gjson.Valid(this.json) == true {
this.reader = gjson.Parse(this.json)
}
return this
SECRET_KEY = this.Get("general.secret_key").String()
}
func (this *Config) Get(path string) *Config {
this.path = &path
return this
}
func (this Configuration) Save() Configuration {
// convert config data to an appropriate json struct
v := Form{Form: this.form}.toJSON(func (el FormElement) string {
a, e := json.Marshal(el.Value)
if e != nil {
return "null"
}
return string(a)
})
func (this *Config) Default(value interface{}) *Config {
if this.path == nil {
// convert back to a map[string]interface{} so that we can stuff in backends config
var tmp map[string]interface{}
json.Unmarshal([]byte(v), &tmp)
tmp["connections"] = this.conn
// let's build a json of the whole struct
j, err := json.Marshal(tmp)
if err != nil {
return this
}
if val := this.reader.Get(*this.path).Value(); val == nil {
this.mu.Lock()
this.json, _ = sjson.Set(this.json, *this.path, value)
this.reader = gjson.Parse(this.json)
this.save()
this.mu.Unlock()
}
return this
}
func (this *Config) Set(value interface{}) *Config {
if this.path == nil {
// deploy the config in our config.json
file, err := os.Create(configPath)
if err != nil {
return this
}
this.mu.Lock()
this.json, _ = sjson.Set(this.json, *this.path, value)
this.reader = gjson.Parse(this.json)
this.save()
this.mu.Unlock()
defer file.Close()
file.Write(PrettyPrint(j))
return this
}
func (this Config) String() string {
return this.reader.Get(*this.path).String()
}
func (this Config) Int() int {
val := this.reader.Get(*this.path).Value()
switch val.(type) {
case float64: return int(val.(float64))
case int64: return int(val.(int64))
case int: return val.(int)
}
return 0
}
func (this Config) Bool() bool {
val := this.reader.Get(*this.path).Value()
switch val.(type) {
case bool: return val.(bool)
}
return false
}
func (this Config) Interface() interface{} {
return this.reader.Get(*this.path).Value()
}
func (this Config) save() {
if this.path == nil {
Log.Error("Config error")
return
}
if gjson.Valid(this.json) == false {
Log.Error("Config error")
return
}
if f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE, os.ModePerm); err == nil {
buf := bytes.NewBuffer(PrettyPrint([]byte(this.json)))
io.Copy(f, buf)
f.Close()
}
}
func (this Config) Scan(p interface{}) error {
content := this.reader.Get(*this.path).String()
return json.Unmarshal([]byte(content), &p)
}
func (this Config) Export() (string, error) {
publicConf := struct {
func (this Configuration) Export() interface{} {
return struct {
Editor string `json:"editor"`
ForkButton bool `json:"fork_button"`
DisplayHidden bool `json:"display_hidden"`
@ -195,14 +334,127 @@ func (this Config) Export() (string, error) {
AutoConnect: this.Get("general.auto_connect").Bool(),
Name: this.Get("general.name").String(),
RememberMe: this.Get("general.remember_me").Bool(),
Connections: this.Get("connections").Interface(),
Connections: this.conn,
EnableSearch: this.Get("features.search.enable").Bool(),
EnableShare: this.Get("features.share.enable").Bool(),
MimeTypes: AllMimeTypes(),
}
j, err := json.Marshal(publicConf)
if err != nil {
return "", err
}
return string(j), nil
}
func (this *Configuration) Get(key string) *Configuration {
var traverse func (forms *[]Form, path []string) *FormElement
traverse = func (forms *[]Form, path []string) *FormElement {
if len(path) == 0 {
return nil
}
for i := range *forms {
currentForm := (*forms)[i]
if currentForm.Title == path[0] {
if len(path) == 2 {
// we are on a leaf
// 1) attempt to get a `formElement`
for j, el := range currentForm.Elmnts {
if el.Name == path[1] {
return &(*forms)[i].Elmnts[j]
}
}
// 2) `formElement` does not exist, let's create it
(*forms)[i].Elmnts = append(currentForm.Elmnts, FormElement{ Name: path[1], Type: "text" })
return &(*forms)[i].Elmnts[len(currentForm.Elmnts)]
} else {
// we are NOT on a leaf, let's continue our tree transversal
return traverse(&(*forms)[i].Form, path[1:])
}
}
}
// append a new `form` if the current key doesn't exist
*forms = append(*forms, Form{ Title: path[0] })
return traverse(forms, path)
}
// increase speed (x4 with our bench) by using a cache
tmp := this.cache.Get(key)
if tmp == nil {
this.currentElement = traverse(&this.form, strings.Split(key, "."))
this.cache.Set(key, this.currentElement)
} else {
this.currentElement = tmp.(*FormElement)
}
return this
}
func (this *Configuration) Default(value interface{}) *Configuration {
if this.currentElement == nil {
return this
}
this.mu.Lock()
if this.currentElement.Default == nil {
this.currentElement.Default = value
this.Save()
} else {
if this.currentElement.Default != value {
Log.Debug("Attempt to set multiple default config value => %+v", this.currentElement)
}
}
this.mu.Unlock()
return this
}
func (this *Configuration) Set(value interface{}) *Configuration {
if this.currentElement == nil {
return this
}
this.mu.Lock()
if this.currentElement.Value != value {
this.currentElement.Value = value
this.Save()
}
this.mu.Unlock()
return this
}
func (this Configuration) String() string {
val := this.Interface()
switch val.(type) {
case string: return val.(string)
case []byte: return string(val.([]byte))
}
return ""
}
func (this Configuration) Int() int {
val := this.Interface()
switch val.(type) {
case float64: return int(val.(float64))
case int64: return int(val.(int64))
case int: return val.(int)
}
return 0
}
func (this Configuration) Bool() bool {
val := this.Interface()
switch val.(type) {
case bool: return val.(bool)
}
return false
}
func (this Configuration) Interface() interface{} {
if this.currentElement == nil {
return nil
}
val := this.currentElement.Value
if val == nil {
val = this.currentElement.Default
}
return val
}
func (this Configuration) MarshalJSON() ([]byte, error) {
return Form{
Form: this.form,
}.MarshalJSON()
}

View File

@ -6,12 +6,14 @@ import (
)
func TestConfigGet(t *testing.T) {
assert.Equal(t, nil, NewConfig().Get("foo").Interface())
assert.Equal(t, nil, NewConfig().Get("foo.bar").Interface())
c := NewConfiguration()
assert.Equal(t, nil, c.Get("foo").Interface())
assert.Equal(t, nil, c.Get("foo.bar").Interface())
}
func TestConfigDefault(t *testing.T) {
c := NewConfig()
c := NewConfiguration()
assert.Equal(t, "test", c.Get("foo.bar").Default("test").Interface())
assert.Equal(t, "test", c.Get("foo.bar").Default("test").String())
assert.Equal(t, "test", c.Get("foo.bar").String())
assert.Equal(t, "test", c.Get("foo.bar").Default("nope").String())
@ -19,59 +21,25 @@ func TestConfigDefault(t *testing.T) {
}
func TestConfigTypeCase(t *testing.T) {
assert.Equal(t, nil, NewConfig().Get("foo.bar.nil").Default(nil).Interface())
assert.Equal(t, true, NewConfig().Get("foo.bar").Default(true).Bool())
assert.Equal(t, 10, NewConfig().Get("foo.bar").Default(10).Int())
assert.Equal(t, "test", NewConfig().Get("foo.bar").Default("test").String())
c := NewConfiguration()
assert.Equal(t, nil, c.Get("foo.bar.nil").Default(nil).Interface())
assert.Equal(t, true, c.Get("foo.bar.bool").Default(true).Bool())
assert.Equal(t, 100, c.Get("foo.bar.int").Default(100).Int())
assert.Equal(t, "test", c.Get("foo.bar.string").Default("test").String())
}
func TestConfigSet(t *testing.T) {
c := NewConfig()
assert.Equal(t, "test", c.Get("foo.bar").Set("test").String())
assert.Equal(t, "valu", c.Get("foo.bar").Set("valu").String())
assert.Equal(t, "valu", c.Get("foo.bar.test.bar.foo").Set("valu").String())
}
func TestConfigScan(t *testing.T) {
c := NewConfig()
c.Get("foo.bar").Default("test")
c.Get("foo.bar2").Default(32)
c.Get("foo.bar3").Default(true)
var data struct {
Bar string `json:"bar"`
Bar2 int `json:"bar2"`
Bar3 bool `json:"bar3"`
}
c.Get("foo").Scan(&data)
assert.Equal(t, "test", data.Bar)
assert.Equal(t, 32, data.Bar2)
assert.Equal(t, true, data.Bar3)
}
func TestConfigSlice(t *testing.T) {
c := NewConfig()
c.Get("connections.-1").Set(map[string]interface{}{"type": "test0", "label": "test0"})
c.Get("connections.-1").Set(map[string]interface{}{"type": "test1", "label": "Test1"})
var data []struct {
Type string `json:"type"`
Label string `json:"label"`
}
c.Get("connections").Scan(&data)
assert.Equal(t, 2, len(data))
assert.Equal(t, "test0", data[0].Type)
assert.Equal(t, "test0", data[0].Label)
assert.Equal(t, "test", Config.Get("foo.bar").Set("test").String())
assert.Equal(t, "valu", Config.Get("foo.bar").Set("valu").String())
assert.Equal(t, "valu", Config.Get("foo.bar.test.bar.foo").Set("valu").String())
}
func BenchmarkGetConfigElement(b *testing.B) {
c := NewConfig()
c := NewConfiguration()
c.Get("foo.bar.test.foo").Set("test")
c.Get("foo.bar.test.bar.foo").Set("valu")
for n := 0; n < b.N; n++ {
c.Get("foo").String()
c.Get("foo.bar.test.foo").String()
}
}

View File

@ -4,9 +4,13 @@ const (
APP_VERSION = "v0.3"
CONFIG_PATH = "data/config/"
PLUGIN_PATH = "data/plugin/"
LOG_PATH = "data/log/"
COOKIE_NAME_AUTH = "auth"
COOKIE_NAME_PROOF = "proof"
COOKIE_NAME_ADMIN = "admin"
COOKIE_PATH_ADMIN = "/admin/api/"
COOKIE_PATH = "/api/"
FILE_INDEX = "./data/public/index.html"
FILE_ASSETS = "./data/public/"
URL_SETUP = "/admin/setup"
)

View File

@ -142,7 +142,7 @@ func verify(something []byte) ([]byte, error) {
func GenerateID(ctx *App) string {
params := ctx.Session
p := "type =>" + params["type"]
p += "salt => " + ctx.Config.Get("general.secret_key").String()
p += "salt => " + SECRET_KEY
p += "host =>" + params["host"]
p += "hostname =>" + params["hostname"]
p += "username =>" + params["username"]

View File

@ -24,7 +24,6 @@ func TestIDGeneration(t *testing.T) {
session["foo"] = "bar"
app := &App{
Session: session,
Config: NewConfig(),
}
id1 := GenerateID(app)

View File

@ -14,6 +14,7 @@ var (
ErrPermissionDenied error = NewError("Permission Denied", 403)
ErrNotValid error = NewError("Not Valid", 405)
ErrNotReachable error = NewError("Cannot Reach Destination", 502)
ErrInvalidPassword = NewError("Invalid Password", 403)
)
type AppError struct {

View File

@ -2,9 +2,25 @@ package common
import (
slog "log"
"fmt"
"time"
"os"
"path/filepath"
)
var logfile *os.File
func init(){
var err error
logPath := filepath.Join(GetCurrentDir(), LOG_PATH)
os.MkdirAll(logPath, os.ModePerm)
logfile, err = os.OpenFile(filepath.Join(logPath, "access.log"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
slog.Printf("ERROR log file: %+v", err)
}
logfile.WriteString("")
}
type LogEntry struct {
Host string `json:"host"`
Method string `json:"method"`
@ -32,28 +48,53 @@ type log struct{
func (l *log) Info(format string, v ...interface{}) {
if l.info && l.enable {
slog.Printf("INFO " + format + "\n", v...)
message := fmt.Sprintf("%s INFO ", l.now())
message = fmt.Sprintf(message + format + "\n", v...)
logfile.WriteString(message)
}
}
func (l *log) Warning(format string, v ...interface{}) {
if l.warn && l.enable {
slog.Printf("WARN " + format + "\n", v...)
message := fmt.Sprintf("%s WARN ", l.now())
message = fmt.Sprintf(message + format + "\n", v...)
logfile.WriteString(message)
}
}
func (l *log) Error(format string, v ...interface{}) {
if l.error && l.enable {
slog.Printf("ERROR " + format + "\n", v...)
message := fmt.Sprintf("%s ERROR ", l.now())
message = fmt.Sprintf(message + format + "\n", v...)
logfile.WriteString(message)
}
}
func (l *log) Debug(format string, v ...interface{}) {
if l.debug && l.enable {
slog.Printf("DEBUG " + format + "\n", v...)
message := fmt.Sprintf("%s DEBUG ", l.now())
message = fmt.Sprintf(message + format + "\n", v...)
logfile.WriteString(message)
}
}
func (l *log) Stdout(format string, v ...interface{}) {
slog.Printf(format + "\n", v...)
if l.enable {
message := fmt.Sprintf("%s MESSAGE: ", l.now())
message = fmt.Sprintf(message + format + "\n", v...)
logfile.WriteString(message)
}
}
func (l *log) now() string {
return time.Now().Format("2006/01/02 15:04:05")
}
func (l *log) Close() {
logfile.Close()
}
func (l *log) SetVisibility(str string) {
switch str {
case "WARNING":

View File

@ -5,6 +5,17 @@ import (
"net/http"
)
const (
PluginTypeBackend = "backend"
PluginTypeMiddleware = "middleware"
)
type Plugin struct {
Type string
Enable bool
}
type Register struct{}
type Get struct{}

35
server/common/token.go Normal file
View File

@ -0,0 +1,35 @@
package common
import (
"time"
)
const (
ADMIN_CLAIM = "ADMIN"
)
type AdminToken struct {
Claim string `json:"token"`
Expire time.Time `json:"time"`
}
func NewAdminToken() AdminToken {
return AdminToken{
Claim: ADMIN_CLAIM,
Expire: time.Now().Add(time.Hour * 24),
}
}
func (this AdminToken) IsAdmin() bool {
if this.Claim != ADMIN_CLAIM {
return false
}
return true
}
func (this AdminToken) IsValid() bool {
if this.Expire.Sub(time.Now()) <= 0 {
return false
}
return true
}

View File

@ -17,6 +17,7 @@ type IBackend interface {
Save(path string, file io.Reader) error
Touch(path string) error
Info() string
LoginForm() Form
}
type File struct {

85
server/ctrl/admin.go Normal file
View File

@ -0,0 +1,85 @@
package ctrl
import (
"encoding/json"
. "github.com/mickael-kerjean/nuage/server/common"
"golang.org/x/crypto/bcrypt"
"io/ioutil"
"net/http"
"time"
)
func AdminSessionGet(ctx App, res http.ResponseWriter, req *http.Request) {
if admin := Config.Get("auth.admin").String(); admin == "" {
SendSuccessResult(res, true)
return
}
obfuscate := func() string{
c, err := req.Cookie(COOKIE_NAME_ADMIN)
if err != nil {
return ""
}
return c.Value
}()
str, err := DecryptString(SECRET_KEY, obfuscate);
if err != nil {
SendSuccessResult(res, false)
return
}
token := AdminToken{}
json.Unmarshal([]byte(str), &token)
if token.IsAdmin() == false || token.IsValid() == false {
SendSuccessResult(res, false)
return
}
SendSuccessResult(res, true)
}
func AdminSessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
// Step 1: Deliberatly make the request slower to make hacking attempt harder for the attacker
time.Sleep(1500*time.Millisecond)
// Step 2: Make sure current user has appropriate access
admin := Config.Get("auth.admin").String()
if admin == "" {
SendErrorResult(res, NewError("Missing admin account, please contact your administrator", 500))
return
}
var params map[string]string
b, _ := ioutil.ReadAll(req.Body)
json.Unmarshal(b, &params)
if err := bcrypt.CompareHashAndPassword([]byte(admin), []byte(params["password"])); err != nil {
SendErrorResult(res, ErrInvalidPassword)
return
}
// Step 3: Send response to the client
body, _ := json.Marshal(NewAdminToken())
obfuscate, err := EncryptString(SECRET_KEY, string(body))
if err != nil {
SendErrorResult(res, err)
return
}
http.SetCookie(res, &http.Cookie{
Name: COOKIE_NAME_ADMIN,
Value: obfuscate,
Path: COOKIE_PATH_ADMIN,
MaxAge: 60*60, // valid for 1 hour
})
SendSuccessResult(res, true)
}
func AdminBackend(ctx App, res http.ResponseWriter, req *http.Request) {
backends := make(map[string]Form)
drivers := Backend.Drivers()
for key := range drivers {
if obj, ok := drivers[key].(interface{ LoginForm() Form }); ok {
backends[key] = obj.LoginForm()
}
}
SendSuccessResult(res, backends)
}

View File

@ -2,15 +2,92 @@ package ctrl
import (
. "github.com/mickael-kerjean/nuage/server/common"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
)
func ConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
c, err := ctx.Config.Export()
var (
logpath = filepath.Join(GetCurrentDir(), LOG_PATH, "access.log")
cachepath = filepath.Join(GetCurrentDir(), CONFIG_PATH, "config.json")
)
func FetchPluginsHandler(ctx App, res http.ResponseWriter, req *http.Request) {
f, err := os.OpenFile(filepath.Join(GetCurrentDir(), PLUGIN_PATH), os.O_RDONLY, os.ModePerm)
if err != nil {
res.Write([]byte("window.CONFIG = {}"))
SendErrorResult(res, err)
return
}
res.Write([]byte("window.CONFIG = "))
res.Write([]byte(c))
files, err := f.Readdir(0)
if err != nil {
SendErrorResult(res, err)
return
}
plugins := make([]string, 0)
for i := 0; i < len(files); i++ {
plugins = append(plugins, files[i].Name())
}
SendSuccessResults(res, plugins)
}
func FetchLogHandler(ctx App, res http.ResponseWriter, req *http.Request) {
file, err := os.OpenFile(logpath, os.O_RDONLY, os.ModePerm)
if err != nil {
SendErrorResult(res, err)
return
}
defer file.Close()
maxSize := req.URL.Query().Get("maxSize")
if maxSize != "" {
cursor := func() int64 {
tmp, err := strconv.Atoi(maxSize)
if err != nil {
return 0
}
return int64(tmp)
}()
for cursor >= 0 {
if _, err := file.Seek(-cursor, io.SeekEnd); err != nil {
break
}
char := make([]byte, 1)
file.Read(char)
if char[0] == 10 || char[0] == 13 { // stop if we find a line
break
}
cursor += 1
}
}
res.Header().Set("Content-Type", "text/plain")
io.Copy(res, file)
}
func PrivateConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
SendSuccessResult(res, Config)
}
func PrivateConfigUpdateHandler(ctx App, res http.ResponseWriter, req *http.Request) {
b, _ := ioutil.ReadAll(req.Body)
b = PrettyPrint(b)
file, err := os.Create(cachepath)
if err != nil {
SendErrorResult(res, err)
return
}
defer file.Close()
if _, err := file.Write(b); err != nil {
SendErrorResult(res, err)
return
}
file.Close()
Config.Load()
SendSuccessResult(res, nil)
}
func PublicConfigHandler(ctx App, res http.ResponseWriter, req *http.Request) {
SendSuccessResult(res, Config.Export())
}

View File

@ -69,7 +69,7 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
SendErrorResult(res, NewError(err.Error(), 500))
return
}
obfuscate, err := EncryptString(ctx.Config.Get("general.secret_key").String(), string(s))
obfuscate, err := EncryptString(SECRET_KEY, string(s))
if err != nil {
SendErrorResult(res, NewError(err.Error(), 500))
return
@ -93,19 +93,23 @@ 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_AUTH,
Value: "",
Path: COOKIE_PATH,
MaxAge: -1,
}
if ctx.Backend != nil {
if obj, ok := ctx.Backend.(interface{ Close() error }); ok {
go obj.Close()
}
}
http.SetCookie(res, &cookie)
http.SetCookie(res, &http.Cookie{
Name: COOKIE_NAME_AUTH,
Value: "",
Path: COOKIE_PATH,
MaxAge: -1,
})
http.SetCookie(res, &http.Cookie{
Name: COOKIE_NAME_ADMIN,
Value: "",
Path: COOKIE_PATH_ADMIN,
MaxAge: -1,
})
SendSuccessResult(res, nil)
}

View File

@ -178,7 +178,7 @@ func ShareVerifyProof(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
if submittedProof.Key != "<nil>" {
if submittedProof.Key != "" {
submittedProof.Id = Hash(submittedProof.Key + "::" + submittedProof.Value)
verifiedProof = append(verifiedProof, submittedProof)
}

View File

@ -39,15 +39,17 @@ func StaticHandler(_path string, ctx App) http.Handler {
func DefaultHandler(_path string, ctx App) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
http.Error(res, "Invalid request method.", 405)
return
}
header := res.Header()
header.Set("Content-Type", "text/html")
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
SecureHeader(&header)
// Redirect to the admin section on first boot to setup the stuff
if req.URL.String() != URL_SETUP && Config.Get("auth.admin").String() == "" {
http.Redirect(res, req, URL_SETUP, 307)
return
}
p := _path
if _, err := os.Open(path.Join(GetCurrentDir(), p+".gz")); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
res.Header().Set("Content-Encoding", "gzip")

View File

@ -1,12 +1,14 @@
package ctrl
import (
"net/http"
. "github.com/mickael-kerjean/nuage/server/common"
. "github.com/mickael-kerjean/nuage/server/middleware"
"github.com/mickael-kerjean/nuage/server/model"
"github.com/mickael-kerjean/net/webdav"
"github.com/mickael-kerjean/mux"
"net/http"
"path/filepath"
"strings"
"time"
)
@ -17,14 +19,28 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
prefix := "/s/" + share_id
req.Header.Del("Content-Type")
if req.Method == "GET" {
if req.URL.Path == prefix {
DefaultHandler(FILE_INDEX, ctx).ServeHTTP(res, req)
return
if req.Method == "GET" && req.URL.Path == prefix {
DefaultHandler(FILE_INDEX, ctx).ServeHTTP(res, req)
return
}
isCrap := func(p string) bool {
if strings.HasPrefix(p, ".") {
return true
}
return false
}(filepath.Base(req.URL.Path))
if isCrap == true {
http.NotFound(res, req)
return
}
var err error
if ctx.Share, err = ExtractShare(req, &ctx, share_id); err != nil {
http.NotFound(res, req)
return
}
if ctx.Session, err = ExtractSession(req, &ctx); err != nil {
http.NotFound(res, req)
return
@ -38,22 +54,21 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
// webdav is WIP
http.NotFound(res, req)
return
Log.Warning("==== REQUEST ('%s'): %s", req.Method, req.URL.Path)
//start := time.Now()
h := &webdav.Handler{
Prefix: "/s/" + share_id,
FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Session["path"]),
FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Share.Path),
LockSystem: webdav.NewMemLS(),
Logger: func(r *http.Request, err error) {
e := func(err error) string{
if err != nil {
return err.Error()
}
return "OK"
}(err)
Log.Info("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e)
//Log.Info("==== REQUEST ('%s' => %d): %s\n", req.Method, time.Now().Sub(start) / (1000 * 1000), req.URL.Path)
// e := func(err error) string{
// if err != nil {
// return err.Error()
// }
// return "OK"
// }(err)
//Log.Info("INFO %s WEBDAV %s %s %s", share_id, req.Method, req.URL.Path, e)
},
}
h.ServeHTTP(res, req)

View File

@ -15,8 +15,6 @@ import (
func main() {
app := App{}
app.Config = NewConfig()
Log.SetVisibility(app.Config.Get("log.level").String())
Init(&app)
}
@ -48,36 +46,47 @@ func Init(a *App) {
session.Handle("/auth/{service}", APIHandler(SessionOAuthBackend, *a)).Methods("GET")
files := r.PathPrefix("/api/files").Subrouter()
files.HandleFunc("/ls", APIHandler(LoggedInOnly(FileLs), *a)).Methods("GET")
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileCat), *a)).Methods("GET")
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileSave), *a)).Methods("POST")
files.HandleFunc("/mv", APIHandler(LoggedInOnly(FileMv), *a)).Methods("GET")
files.HandleFunc("/rm", APIHandler(LoggedInOnly(FileRm), *a)).Methods("GET")
files.HandleFunc("/ls", APIHandler(LoggedInOnly(FileLs), *a)).Methods("GET")
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileCat), *a)).Methods("GET")
files.HandleFunc("/cat", APIHandler(LoggedInOnly(FileSave), *a)).Methods("POST")
files.HandleFunc("/mv", APIHandler(LoggedInOnly(FileMv), *a)).Methods("GET")
files.HandleFunc("/rm", APIHandler(LoggedInOnly(FileRm), *a)).Methods("GET")
files.HandleFunc("/mkdir", APIHandler(LoggedInOnly(FileMkdir), *a)).Methods("GET")
files.HandleFunc("/touch", APIHandler(LoggedInOnly(FileTouch), *a)).Methods("GET")
share := r.PathPrefix("/api/share").Subrouter()
share.HandleFunc("", APIHandler(ShareList, *a)).Methods("GET")
share.HandleFunc("/{share}", APIHandler(ShareUpsert, *a)).Methods("POST")
share.HandleFunc("/{share}", APIHandler(ShareDelete, *a)).Methods("DELETE")
share.HandleFunc("", APIHandler(ShareList, *a)).Methods("GET")
share.HandleFunc("/{share}", APIHandler(ShareUpsert, *a)).Methods("POST")
share.HandleFunc("/{share}", APIHandler(ShareDelete, *a)).Methods("DELETE")
share.HandleFunc("/{share}/proof", APIHandler(ShareVerifyProof, *a)).Methods("POST")
// WEBDAV
r.PathPrefix("/s/{share}").Handler(CtxInjector(WebdavHandler, *a))
// ADMIN
admin := r.PathPrefix("/admin/api").Subrouter()
admin.HandleFunc("/session", CtxInjector(AdminSessionGet, *a)).Methods("GET")
admin.HandleFunc("/session", CtxInjector(AdminSessionAuthenticate, *a)).Methods("POST")
admin.HandleFunc("/backend", CtxInjector(AdminBackend, *a)).Methods("GET")
admin.HandleFunc("/plugin", CtxInjector(AdminOnly(FetchPluginsHandler), *a)).Methods("GET")
admin.HandleFunc("/log", CtxInjector(AdminOnly(FetchLogHandler), *a)).Methods("GET")
admin.HandleFunc("/config", CtxInjector(AdminOnly(PrivateConfigHandler), *a)).Methods("GET")
admin.HandleFunc("/config", CtxInjector(AdminOnly(PrivateConfigUpdateHandler), *a)).Methods("POST")
// APP
r.HandleFunc("/api/config", CtxInjector(ConfigHandler, *a)).Methods("GET")
r.HandleFunc("/api/config", CtxInjector(PublicConfigHandler, *a)).Methods("GET")
r.PathPrefix("/assets").Handler(StaticHandler(FILE_ASSETS, *a)).Methods("GET")
r.PathPrefix("/about").Handler(AboutHandler(*a))
r.PathPrefix("/about").Handler(AboutHandler(*a)).Methods("GET")
r.PathPrefix("/").Handler(DefaultHandler(FILE_INDEX, *a)).Methods("GET")
srv := &http.Server{
Addr: ":" + strconv.Itoa(a.Config.Get("general.port").Int()),
Addr: ":" + strconv.Itoa(Config.Get("general.port").Int()),
Handler: r,
}
Log.Info("STARTING SERVER")
Log.Stdout("STARTING SERVER")
if err := srv.ListenAndServe(); err != nil {
Log.Error("Server start: %v", err)
Log.Stdout("Server start: %v", err)
return
}
}

View File

@ -0,0 +1,45 @@
package middleware
import (
"encoding/json"
. "github.com/mickael-kerjean/nuage/server/common"
"net/http"
)
func LoggedInOnly(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
return func(ctx App, res http.ResponseWriter, req *http.Request) {
if ctx.Backend == nil || ctx.Session == nil {
SendErrorResult(res, NewError("Forbidden", 403))
return
}
fn(ctx, res, req)
}
}
func AdminOnly(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
return func(ctx App, res http.ResponseWriter, req *http.Request) {
if admin := Config.Get("auth.admin").String(); admin != "" {
c, err := req.Cookie(COOKIE_NAME_ADMIN);
if err != nil {
SendErrorResult(res, ErrPermissionDenied)
return
}
str, err := DecryptString(SECRET_KEY, c.Value);
if err != nil {
SendErrorResult(res, ErrPermissionDenied)
return
}
token := AdminToken{}
json.Unmarshal([]byte(str), &token)
if token.IsValid() == false || token.IsAdmin() == false {
SendErrorResult(res, ErrPermissionDenied)
return
}
}
fn(ctx, res, req)
}
}

View File

@ -0,0 +1,12 @@
package middleware
import (
. "github.com/mickael-kerjean/nuage/server/common"
"net/http"
)
func CtxInjector(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.HandlerFunc {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
fn(ctx, res, req)
})
}

View File

@ -16,7 +16,8 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
var err error
start := time.Now()
res.Header().Add("Content-Type", "application/json")
header := res.Header()
header.Add("Content-Type", "application/json")
if ctx.Body, err = ExtractBody(req); err != nil {
SendErrorResult(res, ErrNotValid)
return
@ -39,32 +40,16 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
req.Body.Close()
go func() {
if ctx.Config.Get("log.telemetry").Bool() {
if Config.Get("log.telemetry").Bool() {
go telemetry(req, &resw, start, ctx.Backend.Info())
}
if ctx.Config.Get("log.enable").Bool() {
if Config.Get("log.enable").Bool() {
go logger(req, &resw, start)
}
}()
}
}
func LoggedInOnly(fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
return func(ctx App, res http.ResponseWriter, req *http.Request) {
if ctx.Backend == nil || ctx.Session == nil {
SendErrorResult(res, NewError("Forbidden", 403))
return
}
fn(ctx, res, req)
}
}
func CtxInjector(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.HandlerFunc {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
fn(ctx, res, req)
})
}
func ExtractBody(req *http.Request) (map[string]interface{}, error) {
var body map[string]interface{}
@ -90,7 +75,8 @@ func ExtractShare(req *http.Request, ctx *App, share_id string) (Share, error) {
return Share{}, nil
}
if ctx.Config.Get("features.share.enable").Bool() == false {
if Config.Get("features.share.enable").Bool() == false {
Log.Debug("Share feature isn't enable, contact your administrator")
return Share{}, NewError("Feature isn't enable, contact your administrator", 405)
}

View File

@ -28,9 +28,9 @@ func (d Dropbox) Init(params map[string]string, app *App) (IBackend, error) {
if env := os.Getenv("DROPBOX_CLIENT_ID"); env != "" {
backend.ClientId = env
} else {
backend.ClientId = app.Config.Get("oauth.dropbox.client_id").Default("").String()
backend.ClientId = Config.Get("auth.dropbox.client_id").Default("").String()
}
backend.Hostname = app.Config.Get("general.host").String()
backend.Hostname = Config.Get("general.host").String()
backend.Bearer = params["bearer"]
if backend.ClientId == "" {
@ -45,6 +45,23 @@ func (d Dropbox) Info() string {
return "dropbox"
}
func (d Dropbox) LoginForm() Form {
return Form{
Elmnts: []FormElement{
FormElement{
Name: "type",
Type: "hidden",
Value: "dropbox",
},
FormElement{
Name: "image",
Type: "image",
Value: "/assets/img/dropbox.png",
},
},
}
}
func (d Dropbox) OAuthURL() string {
url := "https://www.dropbox.com/oauth2/authorize?"
url += "client_id=" + d.ClientId

View File

@ -74,6 +74,57 @@ func (f Ftp) Info() string {
return "ftp"
}
func (f Ftp) LoginForm() Form {
return Form{
Elmnts: []FormElement{
FormElement{
Name: "type",
Type: "hidden",
Value: "ftp",
},
FormElement{
Name: "hostname",
Type: "text",
Placeholder: "Hostname*",
},
FormElement{
Name: "username",
Type: "text",
Placeholder: "Username",
},
FormElement{
Name: "password",
Type: "password",
Placeholder: "Password",
},
FormElement{
Name: "advanced",
Type: "enable",
Placeholder: "Advanced",
Target: []string{"ftp_path", "ftp_port", "ftp_conn"},
},
FormElement{
Id: "ftp_path",
Name: "path",
Type: "text",
Placeholder: "Path",
},
FormElement{
Id: "ftp_port",
Name: "port",
Type: "number",
Placeholder: "Port",
},
FormElement{
Id: "ftp_conn",
Name: "conn",
Type: "number",
Placeholder: "Number of connections",
},
},
}
}
func (f Ftp) Home() (string, error) {
return f.client.Getwd()
}

View File

@ -32,9 +32,9 @@ func (g GDrive) Init(params map[string]string, app *App) (IBackend, error) {
config := &oauth2.Config{
Endpoint: google.Endpoint,
ClientID: app.Config.Get("oauth.gdrive.client_id").Default(os.Getenv("GDRIVE_CLIENT_ID")).String(),
ClientSecret: app.Config.Get("oauth.gdrive.client_secret").Default(os.Getenv("GDRIVE_CLIENT_SECRET")).String(),
RedirectURL: app.Config.Get("general.host").String() + "/login",
ClientID: Config.Get("auth.gdrive.client_id").Default(os.Getenv("GDRIVE_CLIENT_ID")).String(),
ClientSecret: Config.Get("auth.gdrive.client_secret").Default(os.Getenv("GDRIVE_CLIENT_SECRET")).String(),
RedirectURL: Config.Get("general.host").String() + "/login",
Scopes: []string{"https://www.googleapis.com/auth/drive"},
}
if config.ClientID == "" {
@ -71,6 +71,23 @@ func (g GDrive) Info() string {
return "googledrive"
}
func (g GDrive) LoginForm() Form {
return Form{
Elmnts: []FormElement{
FormElement{
Name: "type",
Type: "hidden",
Value: "gdrive",
},
FormElement{
Name: "image",
Type: "image",
Value: "/assets/img/google-drive.png",
},
},
}
}
func (g GDrive) OAuthURL() string {
return g.Config.AuthCodeURL("googledrive", oauth2.AccessTypeOnline)
}

View File

@ -112,6 +112,93 @@ func (g Git) Info() string {
return "git"
}
func (g Git) LoginForm() Form {
return Form{
Elmnts: []FormElement{
FormElement{
Name: "type",
Value: "git",
Type: "hidden",
},
FormElement{
Name: "repo",
Type: "text",
Placeholder: "Repository*",
},
FormElement{
Name: "username",
Type: "text",
Placeholder: "Username",
},
FormElement{
Name: "password",
Type: "password",
Placeholder: "Password",
},
FormElement{
Name: "advanced",
Type: "enable",
Placeholder: "Advanced",
Target: []string{
"git_path", "git_passphrase", "git_commit",
"git_branch", "git_author_email", "git_author_name",
"git_committer_email", "git_committer_name",
},
},
FormElement{
Id: "git_path",
Name: "path",
Type: "text",
Placeholder: "Path",
},
FormElement{
Id: "git_passphrase",
Name: "passphrase",
Type: "text",
Placeholder: "Passphrase",
},
FormElement{
Id: "git_commit",
Name: "commit",
Type: "text",
Placeholder: "Commit Format: default to \"{action}({filename}): {path}\"",
},
FormElement{
Id: "git_branch",
Name: "branch",
Type: "text",
Placeholder: "Branch: default to \"master\"",
},
FormElement{
Id: "git_author_email",
Name: "author_email",
Type: "text",
Placeholder: "Author email",
},
FormElement{
Id: "git_author_name",
Name: "author_name",
Type: "text",
Placeholder: "Author name",
},
FormElement{
Id: "git_committer_email",
Name: "committer_email",
Type: "text",
Placeholder: "Committer email",
},
FormElement{
Id: "git_committer_name",
Name: "committer_name",
Type: "text",
Placeholder: "Committer name",
},
},
}
}
func (g Git) Ls(path string) ([]os.FileInfo, error) {
g.git.refresh()
p, err := g.path(path)

View File

@ -57,6 +57,58 @@ func (s S3Backend) Info() string {
return "s3"
}
func (s S3Backend) LoginForm() Form {
return Form{
Elmnts: []FormElement{
FormElement{
Name: "type",
Type: "hidden",
Value: "s3",
},
FormElement{
Name: "access_key_id",
Type: "text",
Placeholder: "Access Key ID*",
},
FormElement{
Name: "secret_access_key",
Type: "text",
Placeholder: "Secret Access Key*",
},
FormElement{
Name: "advanced",
Type: "enable",
Placeholder: "Advanced",
Target: []string{"s3_path", "s3_encryption_key", "s3_region", "s3_endpoint"},
},
FormElement{
Id: "s3_path",
Name: "path",
Type: "text",
Placeholder: "Path",
},
FormElement{
Id: "s3_encryption_key",
Name: "encryption_key",
Type: "text",
Placeholder: "Encryption Key",
},
FormElement{
Id: "s3_region",
Name: "region",
Type: "text",
Placeholder: "Region",
},
FormElement{
Id: "s3_endpoint",
Name: "endpoint",
Type: "text",
Placeholder: "Endpoint",
},
},
}
}
func (s S3Backend) Meta(path string) Metadata {
if path == "/" {
return Metadata{

View File

@ -95,6 +95,58 @@ func (b Sftp) Info() string {
return "sftp"
}
func (b Sftp) LoginForm() Form {
return Form{
Elmnts: []FormElement{
FormElement{
Name: "type",
Type: "hidden",
Value: "sftp",
},
FormElement{
Name: "hostname",
Type: "text",
Placeholder: "Hostname*",
},
FormElement{
Name: "username",
Type: "text",
Placeholder: "Username",
},
FormElement{
Name: "password",
Type: "password",
Placeholder: "Password",
},
FormElement{
Name: "advanced",
Type: "enable",
Placeholder: "Advanced",
Target: []string{"sftp_path", "sftp_port", "sftp_passphrase"},
},
FormElement{
Id: "sftp_path",
Name: "path",
Type: "text",
Placeholder: "Path",
},
FormElement{
Id: "sftp_port",
Name: "port",
Type: "number",
Placeholder: "Port",
},
FormElement{
Id: "sftp_passphrase",
Name: "passphrase",
Type: "text",
Placeholder: "Passphrase",
},
},
}
}
func (b Sftp) Home() (string, error) {
cwd, err := b.SFTPClient.Getwd()
if err != nil {
@ -113,7 +165,7 @@ func (b Sftp) Ls(path string) ([]os.FileInfo, error) {
}
func (b Sftp) Cat(path string) (io.Reader, error) {
remoteFile, err := b.SFTPClient.Open(path)
remoteFile, err := b.SFTPClient.OpenFile(path, os.O_RDONLY)
if err != nil {
return nil, b.err(err)
}

View File

@ -45,6 +45,45 @@ func (w WebDav) Info() string {
return "webdav"
}
func (w WebDav) LoginForm() Form {
return Form{
Elmnts: []FormElement{
FormElement{
Name: "type",
Type: "hidden",
Value: "webdav",
},
FormElement{
Name: "url",
Type: "text",
Placeholder: "Address*",
},
FormElement{
Name: "username",
Type: "text",
Placeholder: "Username",
},
FormElement{
Name: "password",
Type: "password",
Placeholder: "Password",
},
FormElement{
Name: "advanced",
Type: "enable",
Placeholder: "Advanced",
Target: []string{"webdav_path"},
},
FormElement{
Id: "webdav_path",
Name: "path",
Type: "text",
Placeholder: "Path",
},
},
}
}
func (w WebDav) Ls(path string) ([]os.FileInfo, error) {
files := make([]os.FileInfo, 0)
query := `<d:propfind xmlns:d='DAV:'>

View File

@ -9,26 +9,28 @@ import (
func NewBackend(ctx *App, conn map[string]string) (IBackend, error) {
isAllowed := func() bool {
ret := false
var conns [] struct {
Type string `json:"type"`
Hostname string `json:"hostname"`
Path string `json:"path"`
}
ctx.Config.Get("connections").Scan(&conns)
for i := range conns {
if conns[i].Type == conn["type"] {
if conns[i].Hostname != "" && conns[i].Hostname != conn["hostname"] {
continue
} else if conns[i].Path != "" && conns[i].Path != conn["path"] {
continue
} else {
ret = true
break
}
}
}
return ret
return true
// ret := false
// var conns [] struct {
// Type string `json:"type"`
// Hostname string `json:"hostname"`
// Path string `json:"path"`
// }
// Config.Get("connections").Interface()
// Config.Get("connections").Scan(&conns)
// for i := range conns {
// if conns[i].Type == conn["type"] {
// if conns[i].Hostname != "" && conns[i].Hostname != conn["hostname"] {
// continue
// } else if conns[i].Path != "" && conns[i].Path != conn["path"] {
// continue
// } else {
// ret = true
// break
// }
// }
// }
// return ret
}()
if isAllowed == false {
@ -57,6 +59,9 @@ func MapStringInterfaceToMapStringString(m map[string]interface{}) map[string]st
res := make(map[string]string)
for key, value := range m {
res[key] = fmt.Sprintf("%v", value)
if res[key] == "<nil>" {
res[key] = ""
}
}
return res
}

View File

@ -137,7 +137,7 @@ func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
}
time.Sleep(1000 * time.Millisecond)
if err := bcrypt.CompareHashAndPassword([]byte(*s.Password), []byte(proof.Value)); err != nil {
return p, NewError("Invalid Password", 403)
return p, ErrInvalidPassword
}
p.Value = *s.Password
}
@ -188,16 +188,18 @@ func ShareProofVerifier(ctx *App, s Share, proof Proof) (Proof, error) {
p.Message = NewString("We've sent you a message with a verification code")
// Send email
var email struct {
email := struct {
Hostname string `json:"server"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
From string `json:"from"`
}
if err := ctx.Config.Get("email").Scan(&email); err != nil {
Log.Error("ERROR(%+v)", err)
return p, nil
}{
Hostname: Config.Get("email.server").String(),
Port: Config.Get("email.port").Int(),
Username: Config.Get("email.username").String(),
Password: Config.Get("email.password").String(),
From: Config.Get("email.from").String(),
}
m := gomail.NewMessage()

View File

@ -8,7 +8,6 @@ import (
"path/filepath"
"strings"
"io"
"sync"
)
const DAVCachePath = "data/cache/webdav/"
@ -23,7 +22,6 @@ func init() {
type WebdavFs struct {
backend IBackend
path string
mu sync.Mutex
}
func NewWebdavFs(b IBackend, path string) WebdavFs {
@ -34,6 +32,7 @@ func NewWebdavFs(b IBackend, path string) WebdavFs {
}
func (fs WebdavFs) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
Log.Info("MKDIR ('%s')", name)
if name = fs.resolve(name); name == "" {
return os.ErrInvalid
}
@ -41,10 +40,12 @@ func (fs WebdavFs) Mkdir(ctx context.Context, name string, perm os.FileMode) err
}
func (fs WebdavFs) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
Log.Info("OPEN_FILE ('%s')", name)
return NewWebdavNode(name, fs), nil
}
func (fs WebdavFs) RemoveAll(ctx context.Context, name string) error {
Log.Info("RM ('%s')", name)
if name = fs.resolve(name); name == "" {
return os.ErrInvalid
}
@ -52,6 +53,7 @@ func (fs WebdavFs) RemoveAll(ctx context.Context, name string) error {
}
func (fs WebdavFs) Rename(ctx context.Context, oldName, newName string) error {
Log.Info("MV ('%s' => '%s')", oldName, newName)
if oldName = fs.resolve(oldName); oldName == "" {
return os.ErrInvalid
}
@ -62,6 +64,7 @@ func (fs WebdavFs) Rename(ctx context.Context, oldName, newName string) error {
}
func (fs WebdavFs) Stat(ctx context.Context, name string) (os.FileInfo, error) {
Log.Info("STAT ('%s')", name)
if name = fs.resolve(name); name == "" {
return nil, os.ErrInvalid
}
@ -99,6 +102,7 @@ func NewWebdavNode(name string, fs WebdavFs) *WebdavNode {
}
func (w *WebdavNode) Readdir(count int) ([]os.FileInfo, error) {
Log.Info(" => READ_DIR ('%s')", w.path)
var path string
if path = w.fs.resolve(w.path); path == "" {
return nil, os.ErrInvalid
@ -107,27 +111,29 @@ func (w *WebdavNode) Readdir(count int) ([]os.FileInfo, error) {
}
func (w *WebdavNode) Stat() (os.FileInfo, error) {
if w.filewrite != nil {
var path string
var err error
Log.Info(" => STAT ('%s')", w.path)
// if w.filewrite != nil {
// var path stringc
// var err error
if path = w.fs.resolve(w.path); path == "" {
return nil, os.ErrInvalid
}
name := w.filewrite.Name()
w.filewrite.Close()
if w.filewrite, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm); err != nil {
return nil, os.ErrInvalid
}
// if path = w.fs.resolve(w.path); path == "" {
// return nil, os.ErrInvalid
// }
// name := w.filewrite.Name()
// w.filewrite.Close()
// if w.filewrite, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm); err != nil {
// return nil, os.ErrInvalid
// }
if err = w.fs.backend.Save(path, w.filewrite); err != nil {
return nil, err
}
}
// if err = w.fs.backend.Save(path, w.filewrite); err != nil {
// return nil, err
// }
// }
return w.fs.Stat(context.Background(), w.path)
}
func (w *WebdavNode) Close() error {
Log.Info(" => CLOSE ('%s')", w.path)
if w.fileread != nil {
if err := w.cleanup(w.fileread); err != nil {
return err
@ -135,15 +141,27 @@ func (w *WebdavNode) Close() error {
w.fileread = nil
}
if w.filewrite != nil {
if err := w.cleanup(w.filewrite); err != nil {
defer w.cleanup(w.filewrite)
name := w.filewrite.Name()
w.filewrite.Close()
reader, err := os.OpenFile(name, os.O_RDONLY, os.ModePerm);
if err != nil {
return os.ErrInvalid
}
path := w.fs.resolve(w.path)
if path == "" {
return os.ErrInvalid
}
if err := w.fs.backend.Save(path, reader); err != nil {
return err
}
w.filewrite = nil
reader.Close()
}
return nil
}
func (w *WebdavNode) Read(p []byte) (int, error) {
Log.Info(" => READ ('%s')", w.path)
if w.fileread != nil {
return w.fileread.Read(p)
}
@ -151,30 +169,36 @@ func (w *WebdavNode) Read(p []byte) (int, error) {
}
func (w *WebdavNode) Seek(offset int64, whence int) (int64, error) {
Log.Info(" => SEEK ('%s')", w.path)
var path string
var err error
if path = w.fs.resolve(w.path); path == "" {
return -1, os.ErrInvalid
return 0, os.ErrInvalid
}
if w.fileread == nil {
var reader io.Reader
if w.fileread, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_RDWR|os.O_CREATE, os.ModePerm); err != nil {
if w.fileread, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm); err != nil {
return 0, os.ErrInvalid
}
if reader, err = w.fs.backend.Cat(path); err != nil {
return 0, os.ErrInvalid
}
io.Copy(w.fileread, reader)
name := w.fileread.Name()
w.fileread.Close()
w.fileread, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm)
}
return w.fileread.Seek(offset, whence)
}
func (w *WebdavNode) Write(p []byte) (int, error) {
Log.Info(" => WRITE ('%s')", w.path)
var err error
if w.filewrite == nil {
if w.filewrite, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_RDWR|os.O_CREATE, os.ModePerm); err != nil {
if w.filewrite, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm); err != nil {
return 0, os.ErrInvalid
}
}
@ -183,11 +207,7 @@ func (w *WebdavNode) Write(p []byte) (int, error) {
func (w *WebdavNode) cleanup(file *os.File) error {
name := file.Name()
if err := file.Close(); err != nil {
return err
}
if err := os.Remove(name); err != nil {
return err
}
file.Close();
os.Remove(name);
return nil
}

View File

@ -18,7 +18,6 @@ func init() {
}
files, err := file.Readdir(0)
c := NewConfig()
for i:=0; i < len(files); i++ {
name := files[i].Name()
if strings.HasPrefix(name, ".") {
@ -34,8 +33,8 @@ func init() {
Log.Warning("Can't register plugin: %s => %v", name, err)
continue
}
if obj, ok := fn.(func(config *Config)); ok {
obj(c)
if obj, ok := fn.(func(config *Configuration)); ok {
obj(&Config)
}
}
}