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 */
@font-face {
font-family: 'Source Code Pro';
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;
}
/* latin */
@font-face {
font-family: 'Source Code Pro';
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;
}
/* latin-ext */
@font-face {
font-family: 'Source Code Pro';
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;
}
/* latin */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
@ -77,6 +70,10 @@ html {
height: 100%;
}
.center{
text-align: center;
}
a {
color: inherit;
text-decoration: none;

View File

@ -1,5 +1,7 @@
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 "./formbuilder.scss";
@ -21,6 +23,15 @@ export class FormBuilder extends React.Component {
if(Array.isArray(struct)) return null;
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){
return (
<div className="formbuilder">
@ -28,14 +39,25 @@ export class FormBuilder extends React.Component {
key ? <h2 className="no-select">{ format(key) }</h2> : ""
}
{
Object.keys(struct).map((key, index) => {
normal.map((s, index) => {
return (
<div key={key+"-"+index}>
{ this.section(struct[key], key, level + 1) }
<div key={s.key+"-"+index}>
{ this.section(s.data, s.key, level + 1) }
</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>
);
}
@ -78,6 +100,7 @@ export class FormBuilder extends React.Component {
FormObjToJSON(this.props.form)
);
};
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 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} /> );
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"]){
case "text":
const onTextChange = (value) => {
@ -99,14 +122,14 @@ const FormElement = (props) => {
}
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;
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} /> );
$input = ( <Input onChange={(e) => onNumberChange(e.target.value)} {...id} name={struct.label} type="number" value={struct.value || ""} placeholder={struct.placeholder} /> );
break;
case "password":
const onPasswordChange = (value) => {
@ -115,56 +138,53 @@ const FormElement = (props) => {
}
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;
case "bcrypt":
const onBcryptChange = (value) => {
if(value === ""){
return props.onChange(null);
}
bcrypt_password(value).then((hash) => {
return bcrypt_password(value).then((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;
case "hidden":
$input = ( <Input name={props.name} type="hidden" defaultValue={struct.value} /> );
$input = ( <Input name={struct.label} 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} /> );
$input = ( <Input onChange={(e) => props.onChange(e.target.checked)} {...id} name={struct.label} 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} />);
$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;
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;
case "image":
$input = ( <img {...id} src={props.value} /> );
$input = ( <img {...id} src={struct.value} /> );
break;
case "oauth2":
$input = null;
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>
);
};
FormElement.PropTypes = {
render: PropTypes.func.isRequired
};

View File

@ -9,8 +9,8 @@
font-size: 0.95em;
}
input::placeholder{
opacity: 0.5;
input::placeholder, textarea::placeholder{
opacity: 0.6;
}
label.input_type_hidden{
@ -25,4 +25,9 @@
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 { Button } from './button';
export { Container } from './container';
export { NgIf } from './ngif';
export { NgIf, NgShow } from './ngif';
export { Card } from './card';
export { Loader } from './loader';
export { Fab } from './fab';

View File

@ -13,8 +13,6 @@
box-sizing: border-box;
color: inherit;
line-height: 18px;
border-bottom: 2px solid rgba(70, 99, 114, 0.1);
transition: border-color 0.2s ease-out;
&: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{
background: inherit;
border-radius: 0;

View File

@ -27,3 +27,35 @@ NgIf.propTypes = {
cond: PropTypes.bool.isRequired,
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() {
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)){
className += " hasText";
}
const disabledEnter = (e) => {
if(e.key === "Enter" && e.shiftKey === false){
e.preventDefault();

View File

@ -1,6 +1,6 @@
@font-face {
font-family: password;
src: url('textarea.woff');
font-family: 'text-security-disc';
src: url('textarea.woff') format('woff2');
}
.component_textarea{
@ -20,14 +20,22 @@
vertical-align: top;
min-width: 100%;
max-width: 100%;
max-height: 31px; /* Firefox was doing some weird things */
min-height: 30px;
max-height: 30px;
line-height: 18px;
&[name="password"]{
&.hasText{
font-family: 'password';
}
resize: none;
-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);

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 { debounce, throttle } from './backpressure';
export { encrypt, decrypt, bcrypt_password } from './crypto';

View File

@ -40,3 +40,20 @@ function encode_path(path){
export function prepare(path){
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);
}
url(type){
if(type === 'dropbox'){
let url = '/api/session/auth/dropbox';
oauth2(url){
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){

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react';
import { FormBuilder, Loader, Button, Icon } from '../../components/';
import { Config, Log } from '../../model/';
import { FormObjToJSON, notify } from '../../helpers/';
import { FormObjToJSON, notify, format } from '../../helpers/';
import "./logger.scss";
@ -48,12 +48,34 @@ export class LogPage extends React.Component {
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)} />
<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>
<pre style={{height: '350px'}} ref="$log">

View File

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

View File

@ -5,13 +5,14 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { ModalPrompt } from '../../components/';
import { encrypt, decrypt, memory, prompt } from '../../helpers/';
const CREDENTIALS_CACHE = "credentials",
CREDENTIALS_KEY = "credentials_key";
export class Credentials extends React.Component {
constructor(props){
super(props);
const key = memory.get('credentials_key') || ''; // we use a clojure for the "key" because we
// want to persist it in memory, not just in the
// state which is kill whenever react decide
const key = memory.get(CREDENTIALS_KEY) || ''; // we use a clojure for the "key" because we require control
// without the influence of the react component lifecycle
this.state = {
key: key || '',
message: '',
@ -20,7 +21,7 @@ export class Credentials extends React.Component {
if(this.props.remember_me === true){
if(key === ""){
let raw = window.localStorage.hasOwnProperty('credentials');
let raw = window.localStorage.hasOwnProperty(CREDENTIALS_CACHE);
if(raw){
this.promptForExistingPassword();
}else{
@ -33,16 +34,19 @@ export class Credentials extends React.Component {
}
componentWillReceiveProps(new_props){
if(new_props.remember_me === false){
if(window.CONFIG["remember_me"] === false){
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);
}
if(new_props.remember_me === true && this.props.remember_me === false){
this.promptForNewPassword();
}else if(new_props.remember_me === false && this.props.remember_me === true){
memory.set('credentials_key', '');
memory.set(CREDENTIALS_KEY, '');
this.setState({key: ''});
}
}
@ -53,11 +57,11 @@ export class Credentials extends React.Component {
(key) => {
if(!key.trim()) return Promise.reject("Password can\'t be empty");
this.setState({key: key});
memory.set('credentials_key', key);
memory.set(CREDENTIALS_KEY, key);
return this.hidrate_credentials(key);
},
() => {
memory.set('credentials_key', '');
memory.set(CREDENTIALS_KEY, '');
this.setState({key: ''});
},
'password'
@ -68,7 +72,7 @@ export class Credentials extends React.Component {
"Pick a Master Password",
(key) => {
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.saveCreds(this.props.credentials);
});
@ -80,7 +84,7 @@ export class Credentials extends React.Component {
}
hidrate_credentials(key){
let raw = window.localStorage.getItem('credentials');
let raw = window.localStorage.getItem(CREDENTIALS_CACHE);
if(raw){
try{
let credentials = decrypt(raw, key);
@ -96,9 +100,9 @@ export class Credentials extends React.Component {
}
saveCreds(creds){
const key = memory.get('credentials_key');
const key = memory.get(CREDENTIALS_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 { 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 "./form.scss";
import img_drive from "../../assets/img/google-drive.png";
@ -10,25 +10,34 @@ export class Form extends React.Component {
constructor(props){
super(props);
this.state = {
select: window.CONFIG["connections"].length > 2 ? 2 : 0,
backends: JSON.parse(JSON.stringify(window.CONFIG["connections"])).map((backend) => {
return backend;
}),
dummy: null
select: function(){
const connLength = window.CONFIG["connections"].length;
if(connLength < 4) return 0;
else if(connLength < 5) return 1;
return 2;
}(),
backends_enabled: []
};
const select = settings_get("login_tab");
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(){
window.addEventListener("resize", this.rerender);
this.publishState(this.props);
Backend.all().then((b) => {
Backend.all().then((backend) => {
this.props.onLoadingChange(false);
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){
for(let key in props.credentials){
this.state.backends = this.state.backends.map((backend) => {
if(backend["type"] + "_" + backend["label"] === key){
backend = props.credentials[key];
this.state.backends_enabled = this.state.backends_enabled.map((backend) => {
const b = backend[Object.keys(backend)[0]];
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;
});
}
this.setState({backends: this.state.backends});
this.setState({backends_enabled: this.state.backends_enabled});
}
onSubmit(e){
e.preventDefault();
const authData = this.state.backends[this.state.select],
key = authData["type"]+"_"+authData["label"];
this.props.credentials[key] = authData;
this.props.onSubmit(authData, this.props.credentials);
}
onFormUpdate(n, values){
this.state.backends[n] = values;
this.setState({backends: this.state.backends});
}
redirect(service){
this.props.onThirdPartyLogin(service);
const data = () => {
const tmp = this.state.backends_enabled[this.state.select];
return tmp[Object.keys(tmp)[0]];
};
const dataToBeSubmitted = JSON.parse(JSON.stringify(FormObjToJSON(data())));
const key = dataToBeSubmitted["type"] + "_" + dataToBeSubmitted["label"];
delete dataToBeSubmitted.image;
delete dataToBeSubmitted.label;
delete dataToBeSubmitted.advanced;
this.props.credentials[key] = dataToBeSubmitted;
this.props.onSubmit(dataToBeSubmitted, this.props.credentials);
}
onTypeChange(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() {
const _marginTop = () => {
let size = 300;
@ -146,16 +99,17 @@ export class Form extends React.Component {
if(size > 150) return 150;
return size;
};
let className = (window.innerWidth < 600) ? "scroll-x" : "";
return (
<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" : "")}>
{
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.label}
{ backend[key].label.value }
</Button>
);
})
@ -165,415 +119,42 @@ export class Form extends React.Component {
<div>
<form onSubmit={this.onSubmit.bind(this)} autoComplete="off" autoCapitalize="off" spellCheck="false" autoCorrect="off">
{
this.state.backends.map((conn, i) => {
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 (
<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>
<FormBuilder form={form[key]} onChange={this.rerender.bind(this)} key={"form"+i}
render={ ($input, props, struct, onChange) => {
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>
);
}} />
);
})
}
<Button theme="emphasis">CONNECT</Button>
</form>
</div>
</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{
.formbuilder{
fieldset{ padding: 0; border: none; }
legend{ display: none; }
}
padding-top: 5px;
label{
color: rgba(0,0,0,0.4);
font-style: italic;
font-size: 0.9em;
display: block;
text-transform: capitalize;
}
.advanced_form{
max-height: 156px;
overflow-y: auto;
margin-top: 5px
margin-top: 5px;
padding-right: 10px;
}
button.emphasis{
margin-top: 10px;
@ -47,6 +43,11 @@
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()
.then((res) => {
cache.destroy();
this.props.history.push('/');
this.props.history.push('/login');
})
.catch((res) => {
console.warn(res)

View File

@ -41,13 +41,9 @@ func (d *Driver) Drivers() map[string]IBackend {
}
type Nothing struct {}
func (b Nothing) Init(params map[string]string, app *App) (IBackend, error) {
return &Nothing{}, nil
}
func (b Nothing) Info() string {
return "N/A"
}
func (b Nothing) Ls(path string) ([]os.FileInfo, error) {
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 {
return NewError("", 401)
}
func (b Nothing) LoginForm() Form {
return Form{}
}

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package ctrl
import (
"encoding/json"
"fmt"
"github.com/mickael-kerjean/mux"
. "github.com/mickael-kerjean/nuage/server/common"
"github.com/mickael-kerjean/nuage/server/model"
@ -44,13 +45,14 @@ func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {
}
if obj, ok := backend.(interface {
OAuthToken(*map[string]string) error
OAuthToken(*map[string]interface{}) error
}); ok {
err := obj.OAuthToken(&session)
err := obj.OAuthToken(&ctx.Body)
if err != nil {
SendErrorResult(res, NewError("Can't authenticate (OAuth error)", 401))
return
}
session = model.MapStringInterfaceToMapStringString(ctx.Body)
backend, err = model.NewBackend(&ctx, session)
if err != nil {
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 })
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
}
SendSuccessResult(res, obj.OAuthURL())

View File

@ -14,12 +14,14 @@ import (
"strings"
)
var ETAG_INDEX string
func StaticHandler(_path string, ctx App) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
header := res.Header()
header.Set("Content-Type", mime.TypeByExtension(filepath.Ext(req.URL.Path)))
header.Set("Cache-Control", "max-age=2592000")
SecureHeader(&header)
header.Set("X-Content-Type-Options", "nosniff")
if strings.HasSuffix(req.URL.Path, "/") {
http.NotFound(res, req)
@ -39,10 +41,23 @@ 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) {
_path := GetAbsolutePath(_path)
header := res.Header()
header.Set("Content-Type", "text/html")
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
SecureHeader(&header)
header.Set("Cache-Control", "no-cache")
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
if req.URL.String() != URL_SETUP && Config.Get("auth.admin").String() == "" {
@ -50,12 +65,11 @@ func DefaultHandler(_path string, ctx App) http.Handler {
return
}
p := _path
if _, err := os.Open(path.Join(GetCurrentDir(), p+".gz")); err == nil && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
if _, err := os.Open(_path+".gz"); err == nil && strings.Contains(req.Header.Get("Accept-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) {
header := res.Header()
header.Set("Content-Type", "text/html")
SecureHeader(&header)
hash := func(path string) string {
f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
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]
}
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
header.Set("X-XSS-Protection", "1; mode=block")
header.Set("X-Content-Type-Options", "nosniff")
header.Set("X-Frame-Options", "DENY")
page := `<!DOCTYPE html>
<html>
@ -104,7 +108,7 @@ func AboutHandler(ctx App) http.Handler {
App []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 {
pPath := filepath.Join(GetCurrentDir(), PLUGIN_PATH)
file, err := os.Open(pPath)
@ -122,12 +126,12 @@ func AboutHandler(ctx App) http.Handler {
plugins := make([][]string, 0)
plugins = append(plugins, []string {
"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++ {
plugins = append(plugins, []string{
files[i].Name(),
hash(pPath + "/" + files[i].Name()),
hashFile(pPath + "/" + files[i].Name()),
})
}
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")
header.Set("X-Content-Type-Options", "nosniff")
header.Set("X-Frame-Options", "DENY")
func hashFile (path string) string {
f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
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() {
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() {
go logger(req, &resw, start)
@ -121,7 +121,11 @@ func ExtractSession(req *http.Request, ctx *App) (map[string]string, error) {
return res, nil
}
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)
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.Hostname = Config.Get("general.host").String()
backend.Bearer = params["bearer"]
backend.Bearer = params["access_token"]
if backend.ClientId == "" {
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
}
func (d Dropbox) Info() string {
return "dropbox"
}
func (d Dropbox) LoginForm() Form {
return Form{
Elmnts: []FormElement{
@ -54,9 +50,16 @@ func (d Dropbox) LoginForm() Form {
Value: "dropbox",
},
FormElement{
ReadOnly: true,
Name: "oauth2",
Type: "text",
Value: "/api/session/auth/dropbox",
},
FormElement{
ReadOnly: true,
Name: "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
}
func (f Ftp) Info() string {
return "ftp"
}
func (f Ftp) LoginForm() Form {
return Form{
Elmnts: []FormElement{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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