improvement (login): new backend API to generate login form in the frontend

This commit is contained in:
Mickael KERJEAN
2019-01-04 18:14:43 +11:00
parent c213772502
commit a50dbd4724
36 changed files with 453 additions and 767 deletions

View File

@ -1,5 +1,4 @@
/* latin-ext */ /* latin-ext */
@font-face { @font-face {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
font-style: normal; font-style: normal;
@ -8,9 +7,7 @@
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* latin */ /* latin */
@font-face { @font-face {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
font-style: normal; font-style: normal;
@ -19,9 +16,7 @@
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
/* latin-ext */ /* latin-ext */
@font-face { @font-face {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
font-style: normal; font-style: normal;
@ -30,9 +25,7 @@
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* latin */ /* latin */
@font-face { @font-face {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
font-style: normal; font-style: normal;
@ -77,6 +70,10 @@ html {
height: 100%; height: 100%;
} }
.center{
text-align: center;
}
a { a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { Input, Select, Enabler } from './'; import PropTypes from 'prop-types';
import { Input, Textarea, Select, Enabler } from './';
import { FormObjToJSON, bcrypt_password, format } from '../helpers/'; import { FormObjToJSON, bcrypt_password, format } from '../helpers/';
import "./formbuilder.scss"; import "./formbuilder.scss";
@ -21,6 +23,15 @@ export class FormBuilder extends React.Component {
if(Array.isArray(struct)) return null; if(Array.isArray(struct)) return null;
else if(isALeaf(struct) === false){ else if(isALeaf(struct) === false){
const [normal, advanced] = function(s){
let _normal = [];
let _advanced = [];
for (let key in s){
const tmp = {key: key, data: s[key]};
'id' in s[key] ? _advanced.push(tmp) : _normal.push(tmp);
}
return [_normal, _advanced];
}(struct);
if(level <= 1){ if(level <= 1){
return ( return (
<div className="formbuilder"> <div className="formbuilder">
@ -28,14 +39,25 @@ export class FormBuilder extends React.Component {
key ? <h2 className="no-select">{ format(key) }</h2> : "" key ? <h2 className="no-select">{ format(key) }</h2> : ""
} }
{ {
Object.keys(struct).map((key, index) => { normal.map((s, index) => {
return ( return (
<div key={key+"-"+index}> <div key={s.key+"-"+index}>
{ this.section(struct[key], key, level + 1) } { this.section(s.data, s.key, level + 1) }
</div> </div>
); );
}) })
} }
<div className="advanced_form">
{
advanced.map((s, index) => {
return (
<div key={s.key+"-"+index}>
{ this.section(s.data, s.key, level + 1) }
</div>
);
})
}
</div>
</div> </div>
); );
} }
@ -78,6 +100,7 @@ export class FormBuilder extends React.Component {
FormObjToJSON(this.props.form) FormObjToJSON(this.props.form)
); );
}; };
return ( <FormElement render={this.props.render} onChange={onChange.bind(this)} {...id} params={struct} target={target} name={ format(struct.label) } /> ); return ( <FormElement render={this.props.render} onChange={onChange.bind(this)} {...id} params={struct} target={target} name={ format(struct.label) } /> );
} }
@ -90,7 +113,7 @@ export class FormBuilder extends React.Component {
const FormElement = (props) => { const FormElement = (props) => {
const id = props.id !== undefined ? {id: props.id} : {}; const id = props.id !== undefined ? {id: props.id} : {};
let struct = props.params; 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} /> ); let $input = ( <Input onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label} type="text" defaultValue={struct.value} placeholder={struct.placeholder} /> );
switch(props.params["type"]){ switch(props.params["type"]){
case "text": case "text":
const onTextChange = (value) => { const onTextChange = (value) => {
@ -99,14 +122,14 @@ const FormElement = (props) => {
} }
props.onChange(value); props.onChange(value);
}; };
$input = ( <Input onChange={(e) => onTextChange(e.target.value)} {...id} name={props.name} type="text" value={struct.value || ""} placeholder={struct.placeholder}/> ); $input = ( <Input onChange={(e) => onTextChange(e.target.value)} {...id} name={struct.label} type="text" value={struct.value || ""} placeholder={struct.placeholder} readOnly={struct.readonly}/> );
break; break;
case "number": case "number":
const onNumberChange = (value) => { const onNumberChange = (value) => {
value = value === "" ? null : parseInt(value); value = value === "" ? null : parseInt(value);
props.onChange(value); props.onChange(value);
}; };
$input = ( <Input onChange={(e) => onNumberChange(e.target.value)} {...id} name={props.name} type="number" value={struct.value || ""} placeholder={struct.placeholder} /> ); $input = ( <Input onChange={(e) => onNumberChange(e.target.value)} {...id} name={struct.label} type="number" value={struct.value || ""} placeholder={struct.placeholder} /> );
break; break;
case "password": case "password":
const onPasswordChange = (value) => { const onPasswordChange = (value) => {
@ -115,56 +138,53 @@ const FormElement = (props) => {
} }
props.onChange(value); props.onChange(value);
}; };
$input = ( <Input onChange={(e) => onPasswordChange(e.target.value)} {...id} name={props.name} type="password" value={struct.value || ""} placeholder={struct.placeholder} /> ); $input = ( <Input onChange={(e) => onPasswordChange(e.target.value)} {...id} name={struct.label} type="password" value={struct.value || ""} placeholder={struct.placeholder} /> );
break;
case "long_password":
const onLongPasswordChange = (value) => {
if(value === ""){
value = null;
}
props.onChange(value);
};
$input = (
<Textarea {...id} disabledEnter={true} value={struct.value || ""} onChange={(e) => onLongPasswordChange(e.target.value)} type="text" rows="1" name={struct.label} placeholder={struct.placeholder} autoComplete="new-password" />
);
break; break;
case "bcrypt": case "bcrypt":
const onBcryptChange = (value) => { const onBcryptChange = (value) => {
if(value === ""){ if(value === ""){
return props.onChange(null); return props.onChange(null);
} }
bcrypt_password(value).then((hash) => { return bcrypt_password(value).then((hash) => {
props.onChange(hash); props.onChange(hash);
}); });
}; };
$input = ( <Input onChange={(e) => onBcryptChange(e.target.value)} {...id} name={props.name} type="password" defaultValue={struct.value || ""} placeholder={struct.placeholder} /> ); $input = ( <Input onChange={(e) => onBcryptChange(e.target.value)} {...id} name={struct.label} type="password" defaultValue={struct.value || ""} placeholder={struct.placeholder} /> );
break; break;
case "hidden": case "hidden":
$input = ( <Input name={props.name} type="hidden" defaultValue={struct.value} /> ); $input = ( <Input name={struct.label} type="hidden" defaultValue={struct.value} /> );
break; break;
case "boolean": 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} /> ); $input = ( <Input onChange={(e) => props.onChange(e.target.checked)} {...id} name={struct.label} type="checkbox" checked={struct.value === null ? !!struct.default : struct.value} /> );
break; break;
case "select": 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} />); $input = ( <Select onChange={(e) => props.onChange(e.target.value)} {...id} name={struct.label} choices={struct.options} value={struct.value === null ? struct.default : struct.value} placeholder={struct.placeholder} />);
break; break;
case "enable": 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} /> ); $input = ( <Enabler onChange={(e) => props.onChange(e.target.checked)} {...id} name={struct.label} target={props.target} defaultValue={struct.value === null ? struct.default : struct.value} /> );
break; break;
case "image": case "image":
$input = ( <img {...id} src={props.value} /> ); $input = ( <img {...id} src={struct.value} /> );
break;
case "oauth2":
$input = null;
break; break;
} }
if(props.render){ return props.render($input, props, struct, props.onChange.bind(null, null));
return props.render($input, props, struct, props.onChange.bind(null, null)); };
}
FormElement.PropTypes = {
return ( render: PropTypes.func.isRequired
<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

@ -9,8 +9,8 @@
font-size: 0.95em; font-size: 0.95em;
} }
input::placeholder{ input::placeholder, textarea::placeholder{
opacity: 0.5; opacity: 0.6;
} }
label.input_type_hidden{ label.input_type_hidden{
@ -25,4 +25,9 @@
padding: 0 15px; padding: 0 15px;
} }
} }
img{
max-height: 111px;
border: 9px solid rgba(0,0,0,0);
}
} }

View File

@ -4,7 +4,7 @@ export { Input, Select, Enabler } from './input';
export { Textarea } from './textarea'; export { Textarea } from './textarea';
export { Button } from './button'; export { Button } from './button';
export { Container } from './container'; export { Container } from './container';
export { NgIf } from './ngif'; export { NgIf, NgShow } from './ngif';
export { Card } from './card'; export { Card } from './card';
export { Loader } from './loader'; export { Loader } from './loader';
export { Fab } from './fab'; export { Fab } from './fab';

View File

@ -13,8 +13,6 @@
box-sizing: border-box; box-sizing: border-box;
color: inherit; color: inherit;
line-height: 18px; line-height: 18px;
border-bottom: 2px solid rgba(70, 99, 114, 0.1); border-bottom: 2px solid rgba(70, 99, 114, 0.1);
transition: border-color 0.2s ease-out; transition: border-color 0.2s ease-out;
&:focus{ &:focus{
@ -22,6 +20,13 @@
} }
} }
input.component_input[readonly], textarea.component_textarea[readonly]{
border-bottom-style: dashed;
cursor: not-allowed;
font-style: italic;
opacity: 0.8;
}
.component_select{ .component_select{
background: inherit; background: inherit;
border-radius: 0; border-radius: 0;

View File

@ -27,3 +27,35 @@ NgIf.propTypes = {
cond: PropTypes.bool.isRequired, cond: PropTypes.bool.isRequired,
type: PropTypes.string type: PropTypes.string
}; };
export class NgShow extends React.Component {
constructor(props){
super(props);
}
render() {
let clean_prop = Object.assign({}, this.props);
delete clean_prop.cond;
delete clean_prop.children;
delete clean_prop.type;
if(this.props.cond){
if(this.props.type === "inline"){
return <span {...clean_prop}>{this.props.children}</span>;
}else{
return <div {...clean_prop}>{this.props.children}</div>;
}
}else{
return (
<div style={{display: "none"}}>
{this.props.children}
</div>
);
}
}
}
NgShow.propTypes = {
cond: PropTypes.bool.isRequired,
type: PropTypes.string
};

View File

@ -10,10 +10,14 @@ export class Textarea extends React.Component {
render() { render() {
let className = "component_textarea"; let className = "component_textarea";
if(/Firefox/.test(navigator.userAgent)){
className += " firefox";
}
if((this.refs.el !== undefined && this.refs.el.value.length > 0) || (this.props.value !== undefined && this.props.value.length > 0)){ if((this.refs.el !== undefined && this.refs.el.value.length > 0) || (this.props.value !== undefined && this.props.value.length > 0)){
className += " hasText"; className += " hasText";
} }
const disabledEnter = (e) => { const disabledEnter = (e) => {
if(e.key === "Enter" && e.shiftKey === false){ if(e.key === "Enter" && e.shiftKey === false){
e.preventDefault(); e.preventDefault();

View File

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: password; font-family: 'text-security-disc';
src: url('textarea.woff'); src: url('textarea.woff') format('woff2');
} }
.component_textarea{ .component_textarea{
@ -20,14 +20,22 @@
vertical-align: top; vertical-align: top;
min-width: 100%; min-width: 100%;
max-width: 100%; max-width: 100%;
max-height: 31px; /* Firefox was doing some weird things */ min-height: 30px;
max-height: 30px;
line-height: 18px; line-height: 18px;
&[name="password"]{ &[name="password"]{
&.hasText{ resize: none;
font-family: 'password';
}
-webkit-text-security:disc!important; -webkit-text-security:disc!important;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: moz-none;
-ms-user-select: none;
user-select: none;
&.firefox.hasText{
font-family: 'text-security-disc';
}
} }
border-bottom: 2px solid rgba(70, 99, 114, 0.1); border-bottom: 2px solid rgba(70, 99, 114, 0.1);

Binary file not shown.

View File

@ -1,4 +1,4 @@
export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout } from './navigate'; export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout, urlParams } from './navigate';
export { opener } from './mimetype'; export { opener } from './mimetype';
export { debounce, throttle } from './backpressure'; export { debounce, throttle } from './backpressure';
export { encrypt, decrypt, bcrypt_password } from './crypto'; export { encrypt, decrypt, bcrypt_password } from './crypto';

View File

@ -40,3 +40,20 @@ function encode_path(path){
export function prepare(path){ export function prepare(path){
return encodeURIComponent(decodeURIComponent(path)); // to send our url correctly without using directly '/' return encodeURIComponent(decodeURIComponent(path)); // to send our url correctly without using directly '/'
} }
export function urlParams() {
let p = "";
if(window.location.hash){
p += window.location.hash.replace(/^\#/, "");
}
if(window.location.search){
if(p !== "") p += "&";
p += window.location.search.replace(/^\?/, "");
}
return p.split("&").reduce((mem, chunk) => {
const d = chunk.split("=");
if(d.length !== 2) return mem;
mem[decodeURIComponent(d[0])] = decodeURIComponent(d[1]);
return mem;
}, {})
}

View File

@ -7,22 +7,9 @@ class SessionManager{
.then(data => data.result); .then(data => data.result);
} }
url(type){ oauth2(url){
if(type === 'dropbox'){ return http_get(url)
let url = '/api/session/auth/dropbox'; .then(data => data.result);
return http_get(url)
.then(data => data.result);
}else if(type === 'gdrive'){
let url = '/api/session/auth/gdrive';
return http_get(url)
.then(data => data.result);
}else if(type === 'custombackend'){
let url = '/api/session/auth/custombackend';
return http_get(url)
.then(data => data.result);
}else{
return Promise.reject({message: 'no authorization backend', code: 'UNKNOWN_PROVIDER'});
}
} }
authenticate(params){ authenticate(params){

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { FormBuilder } from '../../components/'; import { FormBuilder } from '../../components/';
import { Config } from '../../model/'; import { Config } from '../../model/';
import { format } from '../../helpers';
export class ConfigPage extends React.Component { export class ConfigPage extends React.Component {
constructor(props){ constructor(props){
@ -32,7 +33,7 @@ export class ConfigPage extends React.Component {
} }
onChange(form){ onChange(form){
form.connections = window.CONFIG.connections form.connections = window.CONFIG.connections;
Config.save(form); Config.save(form);
this.setState({refresh: Math.random()}); this.setState({refresh: Math.random()});
} }
@ -40,7 +41,26 @@ export class ConfigPage extends React.Component {
render(){ render(){
return ( return (
<form className="sticky"> <form className="sticky">
<FormBuilder form={this.state.form} onChange={this.onChange.bind(this)} /> <FormBuilder form={this.state.form} onChange={this.onChange.bind(this)} render={ ($input, props, struct, onChange) => {
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>
);
}}/>
</form> </form>
); );
} }

View File

@ -23,7 +23,7 @@ export class DashboardPage extends React.Component {
let [backend, config] = data; let [backend, config] = data;
this.setState({ this.setState({
backend_available: backend, backend_available: backend,
backend_enabled: window.CONFIG.connections.map((conn) => { backend_enabled: window.CONFIG["connections"].map((conn) => {
return createFormBackend(backend, conn); return createFormBackend(backend, conn);
}), }),
config: config config: config
@ -126,6 +126,8 @@ export class DashboardPage extends React.Component {
); );
if(struct.label === "label"){ if(struct.label === "label"){
$checkbox = null; $checkbox = null;
} else if(struct.readonly === true) {
$checkbox = null;
} }
return ( return (
<label className={"no-select input_type_" + props.params["type"]}> <label className={"no-select input_type_" + props.params["type"]}>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { FormBuilder, Loader, Button, Icon } from '../../components/'; import { FormBuilder, Loader, Button, Icon } from '../../components/';
import { Config, Log } from '../../model/'; import { Config, Log } from '../../model/';
import { FormObjToJSON, notify } from '../../helpers/'; import { FormObjToJSON, notify, format } from '../../helpers/';
import "./logger.scss"; import "./logger.scss";
@ -48,12 +48,34 @@ export class LogPage extends React.Component {
let tmp = "access_"; let tmp = "access_";
tmp += new Date().toISOString().substring(0,10).replace(/-/g, ""); tmp += new Date().toISOString().substring(0,10).replace(/-/g, "");
tmp += ".log"; tmp += ".log";
} };
return ( return (
<div className="component_logpage"> <div className="component_logpage">
<h2>Logging { this.state.loading === true ? <Icon style={{height: '40px'}} name="loading"/> : null}</h2> <h2>Logging { this.state.loading === true ? <Icon style={{height: '40px'}} name="loading"/> : null}</h2>
<div style={{minHeight: '150px'}}> <div style={{minHeight: '150px'}}>
<FormBuilder form={this.state.form} onChange={this.onChange.bind(this)} /> <FormBuilder form={this.state.form} onChange={this.onChange.bind(this)}
render={ ($input, props, struct, onChange) => {
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>
);
}} />
</div> </div>
<pre style={{height: '350px'}} ref="$log"> <pre style={{height: '350px'}} ref="$log">

View File

@ -3,9 +3,9 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import './connectpage.scss'; import './connectpage.scss';
import { Session } from '../model/'; import { Session } from '../model/';
import { Container, NgIf, Loader, Notification } from '../components/'; import { Container, NgIf, NgShow, Loader, Notification } from '../components/';
import { ForkMe, RememberMe, Credentials, Form } from './connectpage/'; import { ForkMe, RememberMe, Credentials, Form } from './connectpage/';
import { cache, notify } from '../helpers/'; import { cache, notify, urlParams } from '../helpers/';
import { Alert } from '../components/'; import { Alert } from '../components/';
@ -15,35 +15,27 @@ export class ConnectPage extends React.Component {
this.state = { this.state = {
credentials: {}, credentials: {},
remember_me: window.localStorage.hasOwnProperty('credentials') ? true : false, remember_me: window.localStorage.hasOwnProperty('credentials') ? true : false,
loading: false, loading: true,
doing_a_third_party_login: false doing_a_third_party_login: false
}; };
} }
componentWillMount(){ componentWillMount(){
function getParam(name) { const urlData = urlParams();
const regex = new RegExp("[?&#]" + name.replace(/[\[\]]/g, "\\$&") + "(=([^&#]*)|&|#|$)"); if(Object.keys(urlData).length === 0){
const results = regex.exec(window.location.href); return;
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
} }
const state = getParam('state'); if(!urlData.type){
if(state === "dropbox"){ urlData.type = urlData.state;
this.setState({doing_a_third_party_login: true});
this.authenticate({bearer: getParam('access_token'), type: 'dropbox'});
}else if(state === "googledrive"){
this.setState({doing_a_third_party_login: true});
this.authenticate({code: getParam('code'), type: 'gdrive'});
}else if(state === "custombackend"){
this.setState({doing_a_third_party_login: true});
this.authenticate({code: getParam('code'), type: 'custombackend'});
} }
this.setState({
doing_a_third_party_login: true,
loading: true
}, () => this.authenticate(urlData));
} }
authenticate(params){ authenticate(params){
this.setState({loading: true});
Session.authenticate(params) Session.authenticate(params)
.then(Session.currentUser) .then(Session.currentUser)
.then((user) => { .then((user) => {
@ -64,37 +56,18 @@ export class ConnectPage extends React.Component {
}); });
} }
initiateAuthToThirdParty(source){
if(source === 'dropbox'){
this.setState({loading: true});
Session.url('dropbox').then((url) => {
window.location.href = url;
}).catch((err) => {
this.setState({loading: false});
notify.send(err, 'error');
});
}else if(source === 'google'){
this.setState({loading: true});
Session.url('gdrive').then((url) => {
window.location.href = url;
}).catch((err) => {
this.setState({loading: false});
notify.send(err, 'error');
});
}else if(source === 'custombackend'){
Session.url('custombackend').then((url) => {
window.location.href = url;
}).catch((err) => {
this.setState({loading: false});
notify.send(err, 'error');
});
}
}
onFormSubmit(data, credentials){ onFormSubmit(data, credentials){
this.setState({credentials: credentials}, () => { if('oauth2' in data){
this.authenticate(data); this.setState({loading: true});
}); Session.oauth2(data.oauth2).then((url) => {
window.location.href = url;
});
return;
}
this.setState({
credentials: credentials,
loading: true
}, () => this.authenticate(data));
} }
setRemember(state){ setRemember(state){
@ -105,6 +78,12 @@ export class ConnectPage extends React.Component {
this.setState({credentials: creds}); this.setState({credentials: creds});
} }
setLoading(value){
if(this.state.doing_a_third_party_login !== true){
this.setState({loading: value});
}
}
render() { render() {
return ( return (
<div className="component_page_connect"> <div className="component_page_connect">
@ -115,16 +94,16 @@ export class ConnectPage extends React.Component {
<NgIf cond={this.state.loading === true}> <NgIf cond={this.state.loading === true}>
<Loader/> <Loader/>
</NgIf> </NgIf>
<NgIf cond={this.state.loading === false}> <NgShow cond={this.state.loading === false}>
<ReactCSSTransitionGroup transitionName="form" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={500}> <ReactCSSTransitionGroup transitionName="form" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={500}>
<Form credentials={this.state.credentials} <Form credentials={this.state.credentials}
onThirdPartyLogin={this.initiateAuthToThirdParty.bind(this)} onLoadingChange={this.setLoading.bind(this)}
onSubmit={this.onFormSubmit.bind(this)} /> onSubmit={this.onFormSubmit.bind(this)} />
</ReactCSSTransitionGroup> </ReactCSSTransitionGroup>
<ReactCSSTransitionGroup transitionName="remember" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={5000}> <ReactCSSTransitionGroup transitionName="remember" transitionLeave={false} transitionEnter={false} transitionAppear={true} transitionAppearTimeout={5000}>
<RememberMe state={this.state.remember_me} onChange={this.setRemember.bind(this)}/> <RememberMe state={this.state.remember_me} onChange={this.setRemember.bind(this)}/>
</ReactCSSTransitionGroup> </ReactCSSTransitionGroup>
</NgIf> </NgShow>
<NgIf cond={this.state.doing_a_third_party_login === false}> <NgIf cond={this.state.doing_a_third_party_login === false}>
<Credentials remember_me={this.state.remember_me} <Credentials remember_me={this.state.remember_me}
onRememberMeChange={this.setRemember.bind(this)} onRememberMeChange={this.setRemember.bind(this)}

View File

@ -5,13 +5,14 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { ModalPrompt } from '../../components/'; import { ModalPrompt } from '../../components/';
import { encrypt, decrypt, memory, prompt } from '../../helpers/'; import { encrypt, decrypt, memory, prompt } from '../../helpers/';
const CREDENTIALS_CACHE = "credentials",
CREDENTIALS_KEY = "credentials_key";
export class Credentials extends React.Component { export class Credentials extends React.Component {
constructor(props){ constructor(props){
super(props); super(props);
const key = memory.get('credentials_key') || ''; // we use a clojure for the "key" because we const key = memory.get(CREDENTIALS_KEY) || ''; // we use a clojure for the "key" because we require control
// want to persist it in memory, not just in the // without the influence of the react component lifecycle
// state which is kill whenever react decide
this.state = { this.state = {
key: key || '', key: key || '',
message: '', message: '',
@ -20,7 +21,7 @@ export class Credentials extends React.Component {
if(this.props.remember_me === true){ if(this.props.remember_me === true){
if(key === ""){ if(key === ""){
let raw = window.localStorage.hasOwnProperty('credentials'); let raw = window.localStorage.hasOwnProperty(CREDENTIALS_CACHE);
if(raw){ if(raw){
this.promptForExistingPassword(); this.promptForExistingPassword();
}else{ }else{
@ -33,16 +34,19 @@ export class Credentials extends React.Component {
} }
componentWillReceiveProps(new_props){ componentWillReceiveProps(new_props){
if(new_props.remember_me === false){ if(window.CONFIG["remember_me"] === false){
window.localStorage.clear(); window.localStorage.clear();
}else if(new_props.remember_me === true){ return;
} else if(new_props.remember_me === false){
window.localStorage.clear();
} else if(new_props.remember_me === true){
this.saveCreds(new_props.credentials); this.saveCreds(new_props.credentials);
} }
if(new_props.remember_me === true && this.props.remember_me === false){ if(new_props.remember_me === true && this.props.remember_me === false){
this.promptForNewPassword(); this.promptForNewPassword();
}else if(new_props.remember_me === false && this.props.remember_me === true){ }else if(new_props.remember_me === false && this.props.remember_me === true){
memory.set('credentials_key', ''); memory.set(CREDENTIALS_KEY, '');
this.setState({key: ''}); this.setState({key: ''});
} }
} }
@ -53,11 +57,11 @@ export class Credentials extends React.Component {
(key) => { (key) => {
if(!key.trim()) return Promise.reject("Password can\'t be empty"); if(!key.trim()) return Promise.reject("Password can\'t be empty");
this.setState({key: key}); this.setState({key: key});
memory.set('credentials_key', key); memory.set(CREDENTIALS_KEY, key);
return this.hidrate_credentials(key); return this.hidrate_credentials(key);
}, },
() => { () => {
memory.set('credentials_key', ''); memory.set(CREDENTIALS_KEY, '');
this.setState({key: ''}); this.setState({key: ''});
}, },
'password' 'password'
@ -68,7 +72,7 @@ export class Credentials extends React.Component {
"Pick a Master Password", "Pick a Master Password",
(key) => { (key) => {
if(!key.trim()) return Promise.reject("Password can\'t be empty"); if(!key.trim()) return Promise.reject("Password can\'t be empty");
memory.set('credentials_key', key); memory.set(CREDENTIALS_KEY, key);
this.setState({key: key}, () => { this.setState({key: key}, () => {
this.saveCreds(this.props.credentials); this.saveCreds(this.props.credentials);
}); });
@ -80,7 +84,7 @@ export class Credentials extends React.Component {
} }
hidrate_credentials(key){ hidrate_credentials(key){
let raw = window.localStorage.getItem('credentials'); let raw = window.localStorage.getItem(CREDENTIALS_CACHE);
if(raw){ if(raw){
try{ try{
let credentials = decrypt(raw, key); let credentials = decrypt(raw, key);
@ -96,9 +100,9 @@ export class Credentials extends React.Component {
} }
saveCreds(creds){ saveCreds(creds){
const key = memory.get('credentials_key'); const key = memory.get(CREDENTIALS_KEY);
if(key){ if(key){
window.localStorage.setItem('credentials', encrypt(creds, key)); window.localStorage.setItem(CREDENTIALS_CACHE, encrypt(creds, key));
} }
} }

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Container, Card, NgIf, Input, Button, Textarea, FormBuilder } from "../../components/"; import { Container, Card, NgIf, Input, Button, Textarea, FormBuilder } from "../../components/";
import { gid, settings_get, settings_put, createFormBackend } from "../../helpers/"; import { gid, settings_get, settings_put, createFormBackend, FormObjToJSON } from "../../helpers/";
import { Session, Backend } from "../../model/"; import { Session, Backend } from "../../model/";
import "./form.scss"; import "./form.scss";
import img_drive from "../../assets/img/google-drive.png"; import img_drive from "../../assets/img/google-drive.png";
@ -10,25 +10,34 @@ export class Form extends React.Component {
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {
select: window.CONFIG["connections"].length > 2 ? 2 : 0, select: function(){
backends: JSON.parse(JSON.stringify(window.CONFIG["connections"])).map((backend) => { const connLength = window.CONFIG["connections"].length;
return backend; if(connLength < 4) return 0;
}), else if(connLength < 5) return 1;
dummy: null return 2;
}(),
backends_enabled: []
}; };
const select = settings_get("login_tab"); const select = settings_get("login_tab");
if(select !== null && select < window.CONFIG["connections"].length){ this.state.select = select; } if(select !== null && select < window.CONFIG["connections"].length){ this.state.select = select; }
this.rerender = this.rerender.bind(this); this.rerender = () => this.setState({_refresh: !this.state._refresh});
} }
componentDidMount(){ componentDidMount(){
window.addEventListener("resize", this.rerender); window.addEventListener("resize", this.rerender);
this.publishState(this.props); Backend.all().then((backend) => {
Backend.all().then((b) => { this.props.onLoadingChange(false);
this.setState({ this.setState({
backend_available: b backends_available: backend,
backends_enabled: window.CONFIG["connections"].map((conn) => {
return createFormBackend(backend, conn);
})
}, () => {
return this.publishState(this.props);
}); });
}).catch((err) => {
this.props.error(err);
}); });
} }
@ -45,96 +54,40 @@ export class Form extends React.Component {
publishState(props){ publishState(props){
for(let key in props.credentials){ for(let key in props.credentials){
this.state.backends = this.state.backends.map((backend) => { this.state.backends_enabled = this.state.backends_enabled.map((backend) => {
if(backend["type"] + "_" + backend["label"] === key){ const b = backend[Object.keys(backend)[0]];
backend = props.credentials[key]; if(b["type"].value + "_" + b["label"].value === key){
for(let k in b){
if(props.credentials[key][k]){
b[k].value = props.credentials[key][k];
}
}
} }
return backend; return backend;
}); });
} }
this.setState({backends: this.state.backends}); this.setState({backends_enabled: this.state.backends_enabled});
} }
onSubmit(e){ onSubmit(e){
e.preventDefault(); e.preventDefault();
const authData = this.state.backends[this.state.select], const data = () => {
key = authData["type"]+"_"+authData["label"]; const tmp = this.state.backends_enabled[this.state.select];
return tmp[Object.keys(tmp)[0]];
this.props.credentials[key] = authData; };
this.props.onSubmit(authData, this.props.credentials); const dataToBeSubmitted = JSON.parse(JSON.stringify(FormObjToJSON(data())));
} const key = dataToBeSubmitted["type"] + "_" + dataToBeSubmitted["label"];
delete dataToBeSubmitted.image;
onFormUpdate(n, values){ delete dataToBeSubmitted.label;
this.state.backends[n] = values; delete dataToBeSubmitted.advanced;
this.setState({backends: this.state.backends}); this.props.credentials[key] = dataToBeSubmitted;
} this.props.onSubmit(dataToBeSubmitted, this.props.credentials);
redirect(service){
this.props.onThirdPartyLogin(service);
} }
onTypeChange(n){ onTypeChange(n){
this.setState({select: n}); this.setState({select: n});
} }
rerender(){
this.setState({_refresh: !this.state._refresh});
}
render2() {
const _marginTop = () => {
let size = 300;
const $screen = document.querySelector(".login-form");
if($screen) size = $screen.offsetHeight;
size = Math.round((document.body.offsetHeight - size) / 2);
if(size < 0) return 0;
if(size > 150) return 150;
return size;
};
return (
<Card style={{marginTop: _marginTop()+"px"}} className="no-select component_page_connection_form">
<NgIf cond={ window.CONFIG["connections"].length > 1 }>
<div className={"buttons "+((window.innerWidth < 600) ? "scroll-x" : "")}>
{
this.state.backends.map((backend, i) => {
return (
<Button key={"menu-"+i} className={i === this.state.select ? "active primary" : ""} onClick={this.onTypeChange.bind(this, i)}>
{backend.label}
</Button>
);
})
}
</div>
</NgIf>
<div>
<form onSubmit={this.onSubmit.bind(this)} autoComplete="off" autoCapitalize="off" spellCheck="false" autoCorrect="off">
{
this.state.backends.map((conn, i) => {
console.log(this.state.backend_available);
return (
<NgIf key={"form-"+i} cond={this.state.select === i}>
<FormBuilder form={{"": createFormBackend(this.state.backend_available, conn)}} onChange={() => {this.setState({refresh: !this.state.refresh})}}
render={ ($input, props, struct, onChange) => {
return (
<div style={{width: '100%'}}>
{ $input }
</div>
);
}}/>
</NgIf>
);
})
}
<Button theme="emphasis">CONNECT</Button>
</form>
</div>
</Card>
);
}
// <FormBuilder onChange={() => {}} form={{"": conn}}/>
render() { render() {
const _marginTop = () => { const _marginTop = () => {
let size = 300; let size = 300;
@ -146,434 +99,62 @@ export class Form extends React.Component {
if(size > 150) return 150; if(size > 150) return 150;
return size; return size;
}; };
let className = (window.innerWidth < 600) ? "scroll-x" : "";
return ( return (
<Card style={{marginTop: _marginTop()+"px"}} className="no-select component_page_connection_form"> <Card style={{marginTop: _marginTop()+"px"}} className="no-select component_page_connection_form">
<NgIf cond={ CONFIG["connections"].length > 1 }> <NgIf cond={ window.CONFIG["connections"].length > 1 }>
<div className={"buttons "+((window.innerWidth < 600) ? "scroll-x" : "")}> <div className={"buttons "+((window.innerWidth < 600) ? "scroll-x" : "")}>
{ {
this.state.backends.map((backend, i) => { this.state.backends_enabled.map((backend, i) => {
const key = Object.keys(backend)[0];
return (
<Button key={"menu-"+i} className={i === this.state.select ? "active primary" : ""} onClick={this.onTypeChange.bind(this, i)}>
{ backend[key].label.value }
</Button>
);
})
}
</div>
</NgIf>
<div>
<form onSubmit={this.onSubmit.bind(this)} autoComplete="off" autoCapitalize="off" spellCheck="false" autoCorrect="off">
{
this.state.backends_enabled.map((form, i) => {
const key = Object.keys(form)[0];
if(!form[key]) return null;
else if(this.state.select !== i) return null;
return ( return (
<Button key={"menu-"+i} className={i === this.state.select ? "active primary" : ""} onClick={this.onTypeChange.bind(this, i)}> <FormBuilder form={form[key]} onChange={this.rerender.bind(this)} key={"form"+i}
{backend.label} render={ ($input, props, struct, onChange) => {
</Button> if(struct.type === "image"){
return (
<div className="center">
{ $input }
</div>
);
} else if(struct.enabled === true){
return null;
} else if(struct.label === "advanced") return (
<label style={{color: "rgba(0,0,0,0.4)"}}>
{ $input }
advanced
</label>
);
return (
<label className={"no-select input_type_" + props.params["type"]}>
<div>
{ $input }
</div>
</label>
);
}} />
); );
}) })
} }
</div> <Button theme="emphasis">CONNECT</Button>
</NgIf>
<div>
<form onSubmit={this.onSubmit.bind(this)} autoComplete="off" autoCapitalize="off" spellCheck="false" autoCorrect="off">
{
this.state.backends.map((conn, i) => {
return (
<NgIf key={"form-"+i} cond={this.state.select === i}>
<NgIf cond={conn.type === "webdav"}>
<WebDavForm values={conn} config={CONFIG["connections"][i]} onChange={this.onFormUpdate.bind(this, i)} />
</NgIf>
<NgIf cond={conn.type === "ftp"}>
<FtpForm values={conn} config={CONFIG["connections"][i]} onChange={this.onFormUpdate.bind(this, i)} />
</NgIf>
<NgIf cond={conn.type === "sftp"}>
<SftpForm values={conn} config={CONFIG["connections"][i]} onChange={this.onFormUpdate.bind(this, i)} />
</NgIf>
<NgIf cond={conn.type === "git"}>
<GitForm values={conn} config={CONFIG["connections"][i]} onChange={this.onFormUpdate.bind(this, i)} />
</NgIf>
<NgIf cond={conn.type === "s3"}>
<S3Form values={conn} config={CONFIG["connections"][i]} onChange={this.onFormUpdate.bind(this, i)} />
</NgIf>
<NgIf cond={conn.type === "dropbox"} className="third-party">
<DropboxForm values={conn} config={CONFIG["connections"][i]} onThirdPartyLogin={this.redirect.bind(this)} />
</NgIf>
<NgIf cond={conn.type === "gdrive"} className="third-party">
<GDriveForm values={conn} config={CONFIG["connections"][i]} onThirdPartyLogin={this.redirect.bind(this)} />
</NgIf>
<NgIf cond={conn.type === "custombackend"} className="third-party">
<CustomForm values={conn} config={CONFIG["connections"][i]} onThirdPartyLogin={this.redirect.bind(this)} />
</NgIf>
</NgIf>
);
})
}
</form> </form>
</div> </div>
</Card> </Card>
); );
} }
} }
const WebDavForm = formHelper(function(props){
const onAdvanced = (value) => {
if(value === true){
props.values.path = "";
}else{
delete props.values.path;
}
props.onChange(props.values);
};
const is_advanced = props.advanced(props.values.path);
return (
<div>
<NgIf cond={props.should_appear("url")}>
<Input value={props.values["url"] || ""} onChange={(e) => props.onChange("url", e.target.value)} type={props.input_type("url")} name="url" placeholder="Address*" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("username")}>
<Input value={props.values["username"] || ""} onChange={(e) => props.onChange("username", e.target.value)} type={props.input_type("username")} name="username" placeholder="Username" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("password")}>
<Input value={props.values["password"] || ""} onChange={(e) => props.onChange("password", e.target.value)} type={props.input_type("password")} name="password" placeholder="Password" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("advanced")}>
<label>
<input checked={is_advanced} onChange={(e) => onAdvanced(e.target.checked)} type="checkbox" autoComplete="new-password"/> Advanced
</label>
</NgIf>
<NgIf cond={is_advanced} className="advanced_form">
<NgIf cond={props.should_appear("path")}>
<Input value={props.values["path"] || ""} onChange={(e) => props.onChange("path", e.target.value)} type={props.input_type("path")} name="path" placeholder="Path" autoComplete="new-password" />
</NgIf>
</NgIf>
<Button theme="emphasis">CONNECT</Button>
</div>
);
});
const FtpForm = formHelper(function(props){
const onAdvanced = (value) => {
if(value === true){
props.values.path = "";
props.values.port = "";
}else{
delete props.values.path;
delete props.values.port;
}
props.onChange(props.values);
};
const is_advanced = props.advanced(
props.values.path,
props.values.port
);
return (
<div>
<NgIf cond={props.should_appear("hostname")}>
<Input value={props.values["hostname"] || ""} onChange={(e) => props.onChange("hostname", e.target.value)} type={props.input_type("hostname")} name="hostname" placeholder="Hostname*" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("username")}>
<Input value={props.values["username"] || ""} onChange={(e) => props.onChange("username", e.target.value)} type={props.input_type("username")} name="username" placeholder="Username" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("password")}>
<Input value={props.values["password"] || ""} onChange={(e) => props.onChange("password", e.target.value)} type={props.input_type("password")} name="password" placeholder="Password" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("advanced")}>
<label>
<input checked={is_advanced} onChange={e => onAdvanced(e.target.checked)} type="checkbox" autoComplete="new-password"/> Advanced
</label>
</NgIf>
<NgIf cond={is_advanced} className="advanced_form">
<NgIf cond={props.should_appear("path")}>
<Input value={props.values["path"] || ""} onChange={(e) => props.onChange("path", e.target.value)} type={props.input_type("path")} name="path" placeholder="Path" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("port")}>
<Input value={props.values["port"] || ""} onChange={(e) => props.onChange("port", e.target.value)} type={props.input_type("port")} name="port" placeholder="Port" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("conn")}>
<Input value={props.values["conn"] || ""} onChange={(e) => props.onChange("conn", e.target.value)} type={props.input_type("conn")} name="conn" placeholder="Number of connections" autoComplete="new-password" />
</NgIf>
</NgIf>
<Button type="submit" theme="emphasis">CONNECT</Button>
</div>
);
});
const SftpForm = formHelper(function(props){
const onAdvanced = (value) => {
if(value === true){
props.values.path = "";
props.values.port = "";
props.values.passphrase = "";
}else{
delete props.values.path;
delete props.values.port;
delete props.values.passphrase;
}
props.onChange();
};
const is_advanced = props.advanced(
props.values.path,
props.values.port,
props.values.passphrase
);
return (
<div>
<NgIf cond={props.should_appear("hostname")}>
<Input value={props.values["hostname"] || ""} onChange={(e) => props.onChange("hostname", e.target.value)} value={props.values.hostname || ""} onChange={(e) => props.onChange("hostname", e.target.value)} type={props.input_type("hostname")} name="host" placeholder="Hostname*" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("username")}>
<Input value={props.values["username"] || ""} onChange={(e) => props.onChange("username", e.target.value)} type={props.input_type("username")} name="username" placeholder="Username" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("password")}>
<Textarea disabledEnter={true} value={props.values["password"] || ""} onChange={(e) => props.onChange("password", e.target.value)} type="text" style={props.input_type("password") === "hidden" ? {visibility: "hidden", position: "absolute"} : {} } rows="1" name="password" placeholder="Password" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("advanced")}>
<label>
<input checked={is_advanced} onChange={e => onAdvanced(e.target.checked)} type="checkbox" autoComplete="new-password"/> Advanced
</label>
</NgIf>
<NgIf cond={is_advanced} className="advanced_form">
<NgIf cond={props.should_appear("path")}>
<Input value={props.values["path"] || ""} onChange={(e) => props.onChange("path", e.target.value)} type={props.input_type("path")} name="path" placeholder="Path" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("port")}>
<Input value={props.values["port"] || ""} onChange={(e) => props.onChange("port", e.target.value)} type={props.input_type("port")} name="port" placeholder="Port" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("passphrase")}>
<Input value={props.values["passphrase"] || ""} onChange={(e) => props.onChange("passphrase", e.target.value)} type={props.input_type("passphrase")} name="port" placeholder="Passphrase" autoComplete="new-password" />
</NgIf>
</NgIf>
<Button type="submit" theme="emphasis">CONNECT</Button>
</div>
);
});
const GitForm = formHelper(function(props){
const onAdvanced = (value) => {
if(value === true){
props.values.path = "";
props.values.passphrase = "";
props.values.commit = "";
props.values.branch = "";
props.values.author_email = "";
props.values.author_name = "";
props.values.committer_email = "";
props.values.committer_name = "";
}else{
delete props.values.path;
delete props.values.passphrase;
delete props.values.commit;
delete props.values.branch;
delete props.values.author_email;
delete props.values.author_name;
delete props.values.committer_email;
delete props.values.committer_name;
}
props.onChange();
};
const is_advanced = props.advanced(
props.values.path,
props.values.passphrase,
props.values.commit,
props.values.branch,
props.values.author_email,
props.values.author_name,
props.values.committer_email,
props.values.committer_name
);
return (
<div>
<NgIf cond={props.should_appear("repo")}>
<Input value={props.values["repo"] || ""} onChange={(e) => props.onChange("repo", e.target.value)} type={props.input_type("repo")} name="repo" placeholder="Repository*" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("username")}>
<Input value={props.values["username"] || ""} onChange={(e) => props.onChange("username", e.target.value)} type={props.input_type("username")} name="username" placeholder="Username" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("password")}>
<Textarea disabledEnter={true} value={props.values["password"] || ""} onChange={(e) => props.onChange("password", e.target.value)} type="text" style={props.input_type("password") === "hidden" ? {visibility: "hidden", position: "absolute"} : {} } rows="1" name="password" placeholder="Password" autoComplete="new-password" />
</NgIf>
<Input name="uid" value={gid()} type="hidden" />
<NgIf cond={props.should_appear("advanced")}>
<label>
<input checked={is_advanced} onChange={(e) => onAdvanced(e.target.checked)} type="checkbox" autoComplete="new-password"/> Advanced
</label>
</NgIf>
<NgIf cond={is_advanced} className="advanced_form">
<NgIf cond={props.should_appear("path")}>
<Input value={props.values["path"] || ""} onChange={(e) => props.onChange("path", e.target.value)} type={props.input_type("path")} name="path" placeholder="Path" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("passphrase")}>
<Input value={props.values["passphrase"] || ""} onChange={(e) => props.onChange("passphrase", e.target.value)} type={props.input_type("passphrase")} name="passphrase" placeholder="Passphrase" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("commit")}>
<Input value={props.values["commit"] || ""} onChange={(e) => props.onChange("commit", e.target.value)} type={props.input_type("commit")} name="commit" placeholder='Commit Format: default to \"{action}({filename}): {path}\"' autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("branch")}>
<Input value={props.values["branch"] || ""} onChange={(e) => props.onChange("branch", e.target.value)} type={props.input_type("branch")} name="branch" placeholder='Branch: default to "master"' autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("author_email")}>
<Input value={props.values["author_email"] || ""} onChange={(e) => props.onChange("author_email", e.target.value)} type={props.input_type("author_email")} name="author_email" placeholder="Author email" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("author_name")}>
<Input value={props.values["author_name"] || ""} onChange={(e) => props.onChange("author_name", e.target.value)} type={props.input_type("author_name")} name="author_name" placeholder="Author name" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("committer_email")}>
<Input value={props.values["committer_email"] || ""} onChange={(e) => props.onChange("committer_email", e.target.value)} type={props.input_type("committer_email")} name="committer_email" placeholder="Committer email" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("committer_name")}>
<Input value={props.values["committer_name"] || ""} onChange={(e) => props.onChange("committer_name", e.target.value)} type={props.input_type("committer_name")} name="committer_name" placeholder="Committer name" autoComplete="new-password" />
</NgIf>
</NgIf>
<Button type="submit" theme="emphasis">CONNECT</Button>
</div>
);
});
const S3Form = formHelper(function(props){
const onAdvanced = (value) => {
if(value === true){
props.values.path = "";
props.values.endpoint = "";
props.values.region = "";
props.values.encryption_key = "";
}else{
delete props.values.path;
delete props.values.endpoint;
delete props.values.region;
delete props.values.encryption_key;
}
props.onChange();
};
const is_advanced = props.advanced(
props.values.path,
props.values.endpoint,
props.values.region,
props.values.encryption_key
);
return (
<div>
<NgIf cond={props.should_appear("access_key_id")}>
<Input value={props.values["access_key_id"] || ""} onChange={(e) => props.onChange("access_key_id", e.target.value)} value={props.values.access_key_id || ""} onChange={(e) => props.onChange("access_key_id", e.target.value)} type={props.input_type("access_key_id")} name="access_key_id" placeholder="Access Key ID*" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("secret_access_key")}>
<Input value={props.values["secret_access_key"] || ""} onChange={(e) => props.onChange("secret_access_key", e.target.value)} type={props.input_type("secret_access_key")} name="secret_access_key" placeholder="Secret Access Key*" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("advanced")}>
<label>
<input checked={is_advanced} onChange={(e) => onAdvanced(e.target.checked)} type="checkbox" autoComplete="new-password"/> Advanced
</label>
</NgIf>
<NgIf cond={is_advanced} className="advanced_form">
<NgIf cond={props.should_appear("path")}>
<Input value={props.values["path"] || ""} onChange={(e) => props.onChange("path", e.target.value)} type={props.input_type("path")} name="path" placeholder="Path" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("encryption_key")}>
<Input value={props.values["encryption_key"] || ""} onChange={(e) => props.onChange("encryption_key", e.target.value)} type={props.input_type("encryption_key")} name="encryption_key" placeholder="Encryption Key" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("region")}>
<Input value={props.values["region"] || ""} onChange={(e) => props.onChange("region", e.target.value)} type={props.input_type("region")} name="region" placeholder="Region" autoComplete="new-password" />
</NgIf>
<NgIf cond={props.should_appear("endpoint")}>
<Input value={props.values["endpoint"] || ""} onChange={(e) => props.onChange("endpoint", e.target.value)} type={props.input_type("endpoint")} name="endpoint" placeholder="Endpoint" autoComplete="new-password" />
</NgIf>
</NgIf>
<Button type="submit" theme="emphasis">CONNECT</Button>
</div>
);
});
const DropboxForm = formHelper(function(props){
const redirect = () => {
return props.onThirdPartyLogin("dropbox");
};
if(CONFIG.connections.length === 1 && CONFIG.auto_connect === true){
redirect();
return (
<div>
AUTHENTICATING ...
</div>
);
}
return (
<div>
<div onClick={redirect}>
<img src={img_dropbox} />
</div>
<Button type="button" onClick={redirect} theme="emphasis">LOGIN WITH DROPBOX</Button>
</div>
);
});
const GDriveForm = formHelper(function(props){
const redirect = () => {
return props.onThirdPartyLogin("google");
};
if(CONFIG.connections.length === 1 && CONFIG.auto_connect === true){
redirect();
return (
<div>
AUTHENTICATING ...
</div>
);
}
return (
<div>
<div onClick={redirect}>
<img src={img_drive}/>
</div>
<Button type="button" onClick={redirect} theme="emphasis">LOGIN WITH GOOGLE</Button>
</div>
);
});
const CustomForm = formHelper(function(props){
const redirect = () => {
return props.onThirdPartyLogin("custombackend");
};
if(CONFIG.connections.length === 1 && CONFIG.auto_connect === true){
redirect();
return (
<div>
AUTHENTICATING ...
</div>
);
}
return (
<div>
<Button type="button" onClick={redirect} theme="emphasis">LOGIN</Button>
</div>
);
});
function formHelper(WrappedComponent){
return (props) => {
const helpers = {
should_appear: function(key){
const val = props.config[key];
if(val === undefined) return true;
return false;
},
input_type: function(key){
if(["password", "passphrase", "secret_access_key"].indexOf(key) !== -1){
return "password";
}
return "text";
},
onChange: function(key, value){
let values = props.values;
if(typeof key === "string") values[key] = value;
props.onChange(values);
},
advanced: function(){
let res = false;
for (let i=0; i < arguments.length; i++){
if(arguments[i] !== undefined) {
return true;
}
}
return res;
}
};
return (
<WrappedComponent {...props} {...helpers} />
);
};
}

View File

@ -19,22 +19,18 @@
} }
} }
form{ form{
.formbuilder{
fieldset{ padding: 0; border: none; }
legend{ display: none; }
}
padding-top: 5px; padding-top: 5px;
label{ label{
color: rgba(0,0,0,0.4);
font-style: italic; font-style: italic;
font-size: 0.9em; font-size: 0.9em;
display: block; display: block;
text-transform: capitalize;
} }
.advanced_form{ .advanced_form{
max-height: 156px; max-height: 156px;
overflow-y: auto; overflow-y: auto;
margin-top: 5px margin-top: 5px;
padding-right: 10px;
} }
button.emphasis{ button.emphasis{
margin-top: 10px; margin-top: 10px;
@ -47,6 +43,11 @@
margin: 0; margin: 0;
} }
} }
input[type="checkbox"]{
top: 0;
width: inherit;
margin-right: 5px;
}
} }
} }

View File

@ -13,7 +13,7 @@ export class LogoutPage extends React.Component {
Session.logout() Session.logout()
.then((res) => { .then((res) => {
cache.destroy(); cache.destroy();
this.props.history.push('/'); this.props.history.push('/login');
}) })
.catch((res) => { .catch((res) => {
console.warn(res) console.warn(res)

View File

@ -41,13 +41,9 @@ func (d *Driver) Drivers() map[string]IBackend {
} }
type Nothing struct {} type Nothing struct {}
func (b Nothing) Init(params map[string]string, app *App) (IBackend, error) { func (b Nothing) Init(params map[string]string, app *App) (IBackend, error) {
return &Nothing{}, nil return &Nothing{}, nil
} }
func (b Nothing) Info() string {
return "N/A"
}
func (b Nothing) Ls(path string) ([]os.FileInfo, error) { func (b Nothing) Ls(path string) ([]os.FileInfo, error) {
return nil, NewError("", 401) return nil, NewError("", 401)
} }
@ -69,7 +65,6 @@ func (b Nothing) Touch(path string) error {
func (b Nothing) Save(path string, file io.Reader) error { func (b Nothing) Save(path string, file io.Reader) error {
return NewError("", 401) return NewError("", 401)
} }
func (b Nothing) LoginForm() Form { func (b Nothing) LoginForm() Form {
return Form{} return Form{}
} }

View File

@ -23,7 +23,7 @@ type Configuration struct {
currentElement *FormElement currentElement *FormElement
cache KeyValueStore cache KeyValueStore
form []Form form []Form
conn []map[string]interface{} Conn []map[string]interface{}
} }
type Form struct { type Form struct {
@ -40,7 +40,7 @@ type FormElement struct {
Placeholder string `json:"placeholder,omitempty"` Placeholder string `json:"placeholder,omitempty"`
Opts []string `json:"options,omitempty"` Opts []string `json:"options,omitempty"`
Target []string `json:"target,omitempty"` Target []string `json:"target,omitempty"`
Enabled bool `json:"enabled"` ReadOnly bool `json:"readonly"`
Default interface{} `json:"default"` Default interface{} `json:"default"`
Value interface{} `json:"value"` Value interface{} `json:"value"`
} }
@ -123,7 +123,7 @@ func NewConfiguration() Configuration {
}, },
}, },
}, },
conn: make([]map[string]interface{}, 0), Conn: make([]map[string]interface{}, 0),
} }
} }
@ -219,7 +219,7 @@ func (this *Configuration) Load() {
} }
// Extract enabled backends // Extract enabled backends
this.conn = func(cFile []byte) []map[string]interface{} { this.Conn = func(cFile []byte) []map[string]interface{} {
var d struct { var d struct {
Connections []map[string]interface{} `json:"connections"` Connections []map[string]interface{} `json:"connections"`
} }
@ -286,8 +286,8 @@ func (this *Configuration) Initialise() {
this.Get("general.host").Set(env).String() this.Get("general.host").Set(env).String()
} }
if len(this.conn) == 0 { if len(this.Conn) == 0 {
this.conn = []map[string]interface{}{ this.Conn = []map[string]interface{}{
map[string]interface{}{ map[string]interface{}{
"type": "webdav", "type": "webdav",
"label": "WebDav", "label": "WebDav",
@ -332,7 +332,7 @@ func (this Configuration) Save() Configuration {
} }
return string(a) return string(a)
}) })
v, _ = sjson.Set(v, "connections", this.conn) v, _ = sjson.Set(v, "connections", this.Conn)
// deploy the config in our config.json // deploy the config in our config.json
file, err := os.Create(configPath) file, err := os.Create(configPath)
@ -363,7 +363,7 @@ func (this Configuration) Export() interface{} {
AutoConnect: this.Get("general.auto_connect").Bool(), AutoConnect: this.Get("general.auto_connect").Bool(),
Name: this.Get("general.name").String(), Name: this.Get("general.name").String(),
RememberMe: this.Get("general.remember_me").Bool(), RememberMe: this.Get("general.remember_me").Bool(),
Connections: this.conn, Connections: this.Conn,
EnableSearch: this.Get("features.search.enable").Bool(), EnableSearch: this.Get("features.search.enable").Bool(),
EnableShare: this.Get("features.share.enable").Bool(), EnableShare: this.Get("features.share.enable").Bool(),
MimeTypes: AllMimeTypes(), MimeTypes: AllMimeTypes(),

View File

@ -16,7 +16,6 @@ type IBackend interface {
Mv(from string, to string) error Mv(from string, to string) error
Save(path string, file io.Reader) error Save(path string, file io.Reader) error
Touch(path string) error Touch(path string) error
Info() string
LoginForm() Form LoginForm() Form
} }

View File

@ -78,9 +78,7 @@ func AdminBackend(ctx App, res http.ResponseWriter, req *http.Request) {
drivers := Backend.Drivers() drivers := Backend.Drivers()
for key := range drivers { for key := range drivers {
if obj, ok := drivers[key].(interface{ LoginForm() Form }); ok { backends[key] = drivers[key].LoginForm()
backends[key] = obj.LoginForm()
}
} }
SendSuccessResult(res, backends) SendSuccessResult(res, backends)
} }

View File

@ -2,6 +2,7 @@ package ctrl
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/mickael-kerjean/mux" "github.com/mickael-kerjean/mux"
. "github.com/mickael-kerjean/nuage/server/common" . "github.com/mickael-kerjean/nuage/server/common"
"github.com/mickael-kerjean/nuage/server/model" "github.com/mickael-kerjean/nuage/server/model"
@ -44,13 +45,14 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
} }
if obj, ok := backend.(interface { if obj, ok := backend.(interface {
OAuthToken(*map[string]string) error OAuthToken(*map[string]interface{}) error
}); ok { }); ok {
err := obj.OAuthToken(&session) err := obj.OAuthToken(&ctx.Body)
if err != nil { if err != nil {
SendErrorResult(res, NewError("Can't authenticate (OAuth error)", 401)) SendErrorResult(res, NewError("Can't authenticate (OAuth error)", 401))
return return
} }
session = model.MapStringInterfaceToMapStringString(ctx.Body)
backend, err = model.NewBackend(&ctx, session) backend, err = model.NewBackend(&ctx, session)
if err != nil { if err != nil {
SendErrorResult(res, NewError("Can't authenticate", 401)) SendErrorResult(res, NewError("Can't authenticate", 401))
@ -126,7 +128,7 @@ func SessionOAuthBackend(ctx App, res http.ResponseWriter, req *http.Request) {
} }
obj, ok := b.(interface{ OAuthURL() string }) obj, ok := b.(interface{ OAuthURL() string })
if ok == false { if ok == false {
SendErrorResult(res, NewError("No backend authentication ("+b.Info()+")", 500)) SendErrorResult(res, NewError(fmt.Sprintf("This backend doesn't support oauth: '%s'", a["type"]), 500))
return return
} }
SendSuccessResult(res, obj.OAuthURL()) SendSuccessResult(res, obj.OAuthURL())

View File

@ -14,12 +14,14 @@ import (
"strings" "strings"
) )
var ETAG_INDEX string
func StaticHandler(_path string, ctx App) http.Handler { func StaticHandler(_path string, ctx App) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
header := res.Header() header := res.Header()
header.Set("Content-Type", mime.TypeByExtension(filepath.Ext(req.URL.Path))) header.Set("Content-Type", mime.TypeByExtension(filepath.Ext(req.URL.Path)))
header.Set("Cache-Control", "max-age=2592000") header.Set("Cache-Control", "max-age=2592000")
SecureHeader(&header) header.Set("X-Content-Type-Options", "nosniff")
if strings.HasSuffix(req.URL.Path, "/") { if strings.HasSuffix(req.URL.Path, "/") {
http.NotFound(res, req) http.NotFound(res, req)
@ -39,10 +41,23 @@ func StaticHandler(_path string, ctx App) http.Handler {
func DefaultHandler(_path string, ctx App) http.Handler { func DefaultHandler(_path string, ctx App) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
_path := GetAbsolutePath(_path)
header := res.Header() header := res.Header()
header.Set("Content-Type", "text/html") header.Set("Content-Type", "text/html")
header.Set("Cache-Control", "no-cache, no-store, must-revalidate") header.Set("Cache-Control", "no-cache")
SecureHeader(&header) header.Set("X-XSS-Protection", "1; mode=block")
header.Set("X-Content-Type-Options", "nosniff")
header.Set("X-Frame-Options", "DENY")
if ETAG_INDEX == "" {
ETAG_INDEX = hashFile(_path)
}
if req.Header.Get("If-None-Match") == ETAG_INDEX {
res.WriteHeader(http.StatusNotModified)
return
}
header.Set("Etag", ETAG_INDEX)
// Redirect to the admin section on first boot to setup the stuff // Redirect to the admin section on first boot to setup the stuff
if req.URL.String() != URL_SETUP && Config.Get("auth.admin").String() == "" { if req.URL.String() != URL_SETUP && Config.Get("auth.admin").String() == "" {
@ -50,12 +65,11 @@ func DefaultHandler(_path string, ctx App) http.Handler {
return return
} }
p := _path if _, err := os.Open(_path+".gz"); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
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") res.Header().Set("Content-Encoding", "gzip")
p += ".gz" _path += ".gz"
} }
http.ServeFile(res, req, GetAbsolutePath(p)) http.ServeFile(res, req, _path)
}) })
} }
@ -63,20 +77,10 @@ func AboutHandler(ctx App) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
header := res.Header() header := res.Header()
header.Set("Content-Type", "text/html") header.Set("Content-Type", "text/html")
SecureHeader(&header) header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
header.Set("X-XSS-Protection", "1; mode=block")
hash := func(path string) string { header.Set("X-Content-Type-Options", "nosniff")
f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm) header.Set("X-Frame-Options", "DENY")
if err != nil {
return "__"
}
defer f.Close()
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "__"
}
return base32.HexEncoding.EncodeToString(h.Sum(nil))[:6]
}
page := `<!DOCTYPE html> page := `<!DOCTYPE html>
<html> <html>
@ -104,7 +108,7 @@ func AboutHandler(ctx App) http.Handler {
App []string App []string
Plugins [][]string Plugins [][]string
}{ }{
App: []string{"Nuage " + APP_VERSION, BUILD_NUMBER + "_" + hash(filepath.Join(GetCurrentDir(), "/nuage"))}, App: []string{"Nuage " + APP_VERSION, BUILD_NUMBER + "_" + hashFile(filepath.Join(GetCurrentDir(), "/nuage"))},
Plugins: func () [][]string { Plugins: func () [][]string {
pPath := filepath.Join(GetCurrentDir(), PLUGIN_PATH) pPath := filepath.Join(GetCurrentDir(), PLUGIN_PATH)
file, err := os.Open(pPath) file, err := os.Open(pPath)
@ -122,12 +126,12 @@ func AboutHandler(ctx App) http.Handler {
plugins := make([][]string, 0) plugins := make([][]string, 0)
plugins = append(plugins, []string { plugins = append(plugins, []string {
"config.json", "config.json",
hash(filepath.Join(GetCurrentDir(), "/data/config/config.json")), hashFile(filepath.Join(GetCurrentDir(), "/data/config/config.json")),
}) })
for i:=0; i < len(files); i++ { for i:=0; i < len(files); i++ {
plugins = append(plugins, []string{ plugins = append(plugins, []string{
files[i].Name(), files[i].Name(),
hash(pPath + "/" + files[i].Name()), hashFile(pPath + "/" + files[i].Name()),
}) })
} }
return plugins return plugins
@ -136,8 +140,16 @@ func AboutHandler(ctx App) http.Handler {
}) })
} }
func SecureHeader(header *http.Header) {
header.Set("X-XSS-Protection", "1; mode=block") func hashFile (path string) string {
header.Set("X-Content-Type-Options", "nosniff") f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
header.Set("X-Frame-Options", "DENY") if err != nil {
return "__"
}
defer f.Close()
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "__"
}
return base32.HexEncoding.EncodeToString(h.Sum(nil))[:6]
} }

View File

@ -41,7 +41,7 @@ func APIHandler(fn func(App, http.ResponseWriter, *http.Request), ctx App) http.
go func() { go func() {
if Config.Get("log.telemetry").Bool() { if Config.Get("log.telemetry").Bool() {
go telemetry(req, &resw, start, ctx.Backend.Info()) go telemetry(req, &resw, start, ctx.Session["type"])
} }
if Config.Get("log.enable").Bool() { if Config.Get("log.enable").Bool() {
go logger(req, &resw, start) go logger(req, &resw, start)
@ -121,7 +121,11 @@ func ExtractSession(req *http.Request, ctx *App) (map[string]string, error) {
return res, nil return res, nil
} }
str = cookie.Value str = cookie.Value
str, _ = DecryptString(SECRET_KEY, str) str, err = DecryptString(SECRET_KEY, str)
if err != nil {
// This typically happen when changing the secret key
return res, nil
}
err = json.Unmarshal([]byte(str), &res) err = json.Unmarshal([]byte(str), &res)
return res, err return res, err
} }

View File

@ -31,7 +31,7 @@ func (d Dropbox) Init(params map[string]string, app *App) (IBackend, error) {
backend.ClientId = Config.Get("auth.dropbox.client_id").Default("").String() backend.ClientId = Config.Get("auth.dropbox.client_id").Default("").String()
} }
backend.Hostname = Config.Get("general.host").String() backend.Hostname = Config.Get("general.host").String()
backend.Bearer = params["bearer"] backend.Bearer = params["access_token"]
if backend.ClientId == "" { if backend.ClientId == "" {
return backend, NewError("Missing ClientID: Contact your admin", 502) return backend, NewError("Missing ClientID: Contact your admin", 502)
@ -41,10 +41,6 @@ func (d Dropbox) Init(params map[string]string, app *App) (IBackend, error) {
return backend, nil return backend, nil
} }
func (d Dropbox) Info() string {
return "dropbox"
}
func (d Dropbox) LoginForm() Form { func (d Dropbox) LoginForm() Form {
return Form{ return Form{
Elmnts: []FormElement{ Elmnts: []FormElement{
@ -54,9 +50,16 @@ func (d Dropbox) LoginForm() Form {
Value: "dropbox", Value: "dropbox",
}, },
FormElement{ FormElement{
ReadOnly: true,
Name: "oauth2",
Type: "text",
Value: "/api/session/auth/dropbox",
},
FormElement{
ReadOnly: true,
Name: "image", Name: "image",
Type: "image", Type: "image",
Value: "/assets/img/dropbox.png", Value: "data:image/svg+xml;utf8;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDIuNCAzOS41IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KICA8cG9seWdvbiBmaWxsPSIjMDA3RUU1IiBwb2ludHM9IjEyLjUsMCAwLDguMSA4LjcsMTUuMSAyMS4yLDcuMyIvPgo8cG9seWdvbiBmaWxsPSIjMDA3RUU1IiBwb2ludHM9IjAsMjEuOSAxMi41LDMwLjEgMjEuMiwyMi44IDguNywxNS4xIi8+Cjxwb2x5Z29uIGZpbGw9IiMwMDdFRTUiIHBvaW50cz0iMjEuMiwyMi44IDMwLDMwLjEgNDIuNCwyMiAzMy44LDE1LjEiLz4KPHBvbHlnb24gZmlsbD0iIzAwN0VFNSIgcG9pbnRzPSI0Mi40LDguMSAzMCwwIDIxLjIsNy4zIDMzLjgsMTUuMSIvPgo8cG9seWdvbiBmaWxsPSIjMDA3RUU1IiBwb2ludHM9IjIxLjMsMjQuNCAxMi41LDMxLjcgOC44LDI5LjIgOC44LDMyIDIxLjMsMzkuNSAzMy44LDMyIDMzLjgsMjkuMiAzMCwzMS43Ii8+Cjwvc3ZnPgo=",
}, },
}, },
} }

View File

@ -70,10 +70,6 @@ func (f Ftp) Init(params map[string]string, app *App) (IBackend, error) {
return backend, nil return backend, nil
} }
func (f Ftp) Info() string {
return "ftp"
}
func (f Ftp) LoginForm() Form { func (f Ftp) LoginForm() Form {
return Form{ return Form{
Elmnts: []FormElement{ Elmnts: []FormElement{

View File

@ -67,10 +67,6 @@ func (g GDrive) Init(params map[string]string, app *App) (IBackend, error) {
return backend, nil return backend, nil
} }
func (g GDrive) Info() string {
return "googledrive"
}
func (g GDrive) LoginForm() Form { func (g GDrive) LoginForm() Form {
return Form{ return Form{
Elmnts: []FormElement{ Elmnts: []FormElement{
@ -80,16 +76,23 @@ func (g GDrive) LoginForm() Form {
Value: "gdrive", Value: "gdrive",
}, },
FormElement{ FormElement{
ReadOnly: true,
Name: "oauth2",
Type: "text",
Value: "/api/session/auth/gdrive",
},
FormElement{
ReadOnly: true,
Name: "image", Name: "image",
Type: "image", Type: "image",
Value: "/assets/img/google-drive.png", Value: "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTM5IDEyMC40IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KICA8cGF0aCBkPSJtMjQuMiAxMjAuNC0yNC4yLTQxLjkgNDUuMy03OC41IDI0LjIgNDEuOXoiIGZpbGw9IiMwZGE5NjAiLz4KICA8cGF0aCBkPSJtNTguOSA2MC4yIDEwLjYtMTguMy0yNC4yLTQxLjl6IiBmaWxsPSIjMGRhOTYwIi8+CiAgPHBhdGggZD0ibTI0LjIgMTIwLjQgMjQuMi00MS45aDkwLjZsLTI0LjIgNDEuOXoiIGZpbGw9IiMyZDZmZGQiLz4KICA8cGF0aCBkPSJtNjkuNSA3OC41aC0yMS4xbDEwLjUtMTguMy0zNC43IDYwLjJ6IiBmaWxsPSIjMmQ2ZmRkIi8+ICAKICA8cGF0aCBkPSJtMTM5IDc4LjVoLTQ4LjRsLTQ1LjMtNzguNWg0OC40eiIgZmlsbD0iI2ZmZDI0ZCIvPgogIDxwYXRoIGQ9Im05MC42IDc4LjVoNDguNGwtNTguOS0xOC4zeiIgZmlsbD0iI2ZmZDI0ZCIvPgo8L3N2Zz4K",
}, },
}, },
} }
} }
func (g GDrive) OAuthURL() string { func (g GDrive) OAuthURL() string {
return g.Config.AuthCodeURL("googledrive", oauth2.AccessTypeOnline) return g.Config.AuthCodeURL("gdrive", oauth2.AccessTypeOnline)
} }
func (g GDrive) OAuthToken(ctx *map[string]interface{}) error { func (g GDrive) OAuthToken(ctx *map[string]interface{}) error {

View File

@ -108,10 +108,6 @@ func (git Git) Init(params map[string]string, app *App) (IBackend, error) {
return g, nil return g, nil
} }
func (g Git) Info() string {
return "git"
}
func (g Git) LoginForm() Form { func (g Git) LoginForm() Form {
return Form{ return Form{
Elmnts: []FormElement{ Elmnts: []FormElement{
@ -132,7 +128,7 @@ func (g Git) LoginForm() Form {
}, },
FormElement{ FormElement{
Name: "password", Name: "password",
Type: "password", Type: "long_password",
Placeholder: "Password", Placeholder: "Password",
}, },
FormElement{ FormElement{

View File

@ -53,10 +53,6 @@ func (s S3Backend) Init(params map[string]string, app *App) (IBackend, error) {
return backend, nil return backend, nil
} }
func (s S3Backend) Info() string {
return "s3"
}
func (s S3Backend) LoginForm() Form { func (s S3Backend) LoginForm() Form {
return Form{ return Form{
Elmnts: []FormElement{ Elmnts: []FormElement{

View File

@ -41,7 +41,6 @@ func (s Sftp) Init(params map[string]string, app *App) (IBackend, error) {
params["password"], params["password"],
params["passphrase"], params["passphrase"],
} }
if p.port == "" { if p.port == "" {
p.port = "22" p.port = "22"
} }
@ -91,10 +90,6 @@ func (s Sftp) Init(params map[string]string, app *App) (IBackend, error) {
return &s, nil return &s, nil
} }
func (b Sftp) Info() string {
return "sftp"
}
func (b Sftp) LoginForm() Form { func (b Sftp) LoginForm() Form {
return Form{ return Form{
Elmnts: []FormElement{ Elmnts: []FormElement{
@ -115,7 +110,7 @@ func (b Sftp) LoginForm() Form {
}, },
FormElement{ FormElement{
Name: "password", Name: "password",
Type: "password", Type: "long_password",
Placeholder: "Password", Placeholder: "Password",
}, },
FormElement{ FormElement{

View File

@ -41,10 +41,6 @@ func (w WebDav) Init(params map[string]string, app *App) (IBackend, error) {
return backend, nil return backend, nil
} }
func (w WebDav) Info() string {
return "webdav"
}
func (w WebDav) LoginForm() Form { func (w WebDav) LoginForm() Form {
return Form{ return Form{
Elmnts: []FormElement{ Elmnts: []FormElement{

View File

@ -9,32 +9,39 @@ import (
func NewBackend(ctx *App, conn map[string]string) (IBackend, error) { func NewBackend(ctx *App, conn map[string]string) (IBackend, error) {
isAllowed := func() bool { isAllowed := func() bool {
return true // by default, a hacker could use filestash to establish connections outside of what's
// ret := false // define in the config file. We need to prevent this
// var conns [] struct { possibilities := make([]map[string]interface{}, 0)
// Type string `json:"type"` for i:=0; i< len(Config.Conn); i++ {
// Hostname string `json:"hostname"` d := Config.Conn[i]
// Path string `json:"path"` if d["type"] != conn["type"] {
// } continue
// Config.Get("connections").Interface() }
// Config.Get("connections").Scan(&conns) if val, ok := d["hostname"]; ok == true {
// for i := range conns { if val != conn["hostname"] {
// if conns[i].Type == conn["type"] { continue
// if conns[i].Hostname != "" && conns[i].Hostname != conn["hostname"] { }
// continue }
// } else if conns[i].Path != "" && conns[i].Path != conn["path"] { if val, ok := d["path"]; ok == true {
// continue if val != conn["path"] {
// } else { continue
// ret = true }
// break }
// } if val, ok := d["url"]; ok == true {
// } if val != conn["url"] {
// } continue
// return ret }
}() }
possibilities = append(possibilities, Config.Conn[i])
}
if len(possibilities) > 0 {
return true
}
return false
}
if isAllowed == false { if isAllowed() == false {
return Backend.Get(BACKEND_NIL).Init(conn, ctx) return Backend.Get(BACKEND_NIL), ErrNotAllowed
} }
return Backend.Get(conn["type"]).Init(conn, ctx) return Backend.Get(conn["type"]).Init(conn, ctx)
} }

View File

@ -87,4 +87,4 @@ if (process.env.NODE_ENV === 'production') {
config.devtool = '#inline-source-map'; config.devtool = '#inline-source-map';
} }
module.exports = config; module.exports = config;