mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-03 13:11:46 +08:00
feature (setup): complete rewrite of the setup wizard adding features such as automatic DNS
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Input, Button, Container, Icon } from '../../components/';
|
import { Input, Button, Container, Icon, NgIf, Loader } from '../../components/';
|
||||||
import { Config, Admin } from '../../model/';
|
import { Config, Admin } from '../../model/';
|
||||||
import { notify, FormObjToJSON, alert, prompt } from '../../helpers';
|
import { notify, FormObjToJSON, alert, prompt } from '../../helpers';
|
||||||
import { bcrypt_password } from '../../helpers/bcrypt';
|
import { bcrypt_password } from '../../helpers/bcrypt';
|
||||||
@ -12,116 +12,368 @@ export class SetupPage extends React.Component {
|
|||||||
constructor(props){
|
constructor(props){
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
stage: 0,
|
busy: false,
|
||||||
password: "",
|
|
||||||
enable_telemetry: false,
|
|
||||||
creating_password: false
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createPassword(e){
|
componentDidMount() {
|
||||||
this.setState({creating_password: true});
|
|
||||||
e.preventDefault();
|
|
||||||
const enableLog = (value) => {
|
|
||||||
Config.all().then((config) => {
|
|
||||||
config.log.telemetry.value = value;
|
|
||||||
config = FormObjToJSON(config);
|
|
||||||
config.connections = window.CONFIG.connections;
|
|
||||||
Config.save(config, false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const start = (e) => {
|
const start = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.history.push("/");
|
this.props.history.push("/");
|
||||||
};
|
};
|
||||||
Config.all().then((config) => {
|
|
||||||
this.setState({enable_telemetry: config.log.telemetry.value}, () => {
|
|
||||||
if(this.state.enable_telemetry === true) return;
|
|
||||||
this.unlisten = this.props.history.listen((location, action) => {
|
|
||||||
this.unlisten();
|
|
||||||
alert.now((
|
|
||||||
<div>
|
|
||||||
<p style={{textAlign: 'justify'}}>
|
|
||||||
Help making this software better by sending crash reports and anonymous usage statistics
|
|
||||||
</p>
|
|
||||||
<form onSubmit={start.bind(this)} style={{fontSize: '0.9em', marginTop: '10px'}}>
|
|
||||||
<label>
|
|
||||||
<Input type="checkbox" style={{width: 'inherit', marginRight: '10px'}} onChange={(e) => enableLog(e.target.checked)} defaultChecked={this.state.enable_telemetry} />
|
|
||||||
I accept but the data is not to be share with any third party
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
bcrypt_password(this.state.password)
|
Config.all().then((config) => {
|
||||||
.then((hash) => {
|
if(config.log.telemetry.value === true) return;
|
||||||
config.auth.admin.value = hash;
|
this.unlisten = this.props.history.listen((location, action) => {
|
||||||
config = FormObjToJSON(config);
|
this.unlisten();
|
||||||
config.connections = window.CONFIG.connections;
|
alert.now((
|
||||||
Config.save(config, false)
|
<div>
|
||||||
.then(() => Admin.login(this.state.password))
|
<p style={{textAlign: 'justify'}}>
|
||||||
.then(() => this.setState({stage: 1, creating_password: false}))
|
Help making this software better by sending crash reports and anonymous usage statistics
|
||||||
.catch((err) => {
|
</p>
|
||||||
notify.send(err && err.message, "error");
|
<form onSubmit={start.bind(this)} style={{fontSize: '0.9em', marginTop: '10px'}}>
|
||||||
this.setState({creating_password: false});
|
<label>
|
||||||
});
|
<Input type="checkbox" style={{width: 'inherit', marginRight: '10px'}} onChange={(e) => this.enableLog(e.target.checked)} defaultChecked={config.log.telemetry.value} />
|
||||||
})
|
I accept but the data is not to be share with any third party
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdminPassword(p, done){
|
||||||
|
this.setState({busy: true});
|
||||||
|
Config.all().then((config) => {
|
||||||
|
return bcrypt_password(p).then((hash) => {
|
||||||
|
config = FormObjToJSON(config);
|
||||||
|
config.connections = window.CONFIG.connections;
|
||||||
|
config.auth.admin = hash;
|
||||||
|
Config.save(config, false)
|
||||||
|
.then(() => Admin.login(p))
|
||||||
|
.then(() => this.setState({busy: false}, done))
|
||||||
|
.catch((err) => {
|
||||||
|
this.setState({busy: false});
|
||||||
|
notify.send(err && err.message, "error");
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
this.setState({busy: false});
|
||||||
|
notify.send("Hash error: " + JSON.stringify(err), "error");
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
notify.send(err && err.message, "error");
|
||||||
|
this.setState({busy: false});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onExposeInstance(choice, done){
|
||||||
|
this.setState({busy: true});
|
||||||
|
return Config.all().then((config) => {
|
||||||
|
config = FormObjToJSON(config);
|
||||||
|
config.connections = window.CONFIG.connections;
|
||||||
|
switch(choice){
|
||||||
|
case "tunnel":
|
||||||
|
config.features.server.enable_tunnel = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
config.features.server.enable_tunnel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Config.save(config, false)
|
||||||
|
.then(() => this.setState({busy: false}, done))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notify.send("Hash error: " + JSON.stringify(err), "error");
|
notify.send(err && err.message, "error");
|
||||||
this.setState({creating_password: false});
|
this.setState({busy: false});
|
||||||
});
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
notify.send(err && err.message, "error");
|
||||||
|
this.setState({busy: false});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
enableLog(value){
|
enableLog(value){
|
||||||
Config.all().then((config) => {
|
Config.all().then((config) => {
|
||||||
config.log.telemetry.value = value;
|
|
||||||
config = FormObjToJSON(config);
|
config = FormObjToJSON(config);
|
||||||
config.connections = window.CONFIG.connections;
|
config.connections = window.CONFIG.connections;
|
||||||
|
config.log.telemetry = value;
|
||||||
Config.save(config, false);
|
Config.save(config, false);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
start(e){
|
summaryCall(){
|
||||||
e.preventDefault();
|
this.setState({busy: true});
|
||||||
this.props.history.push("/");
|
return Config.all().then((config) => {
|
||||||
|
this.setState({busy: false});
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name_success": "SSL is configured properly",
|
||||||
|
"name_failure": "SSL is not configured properly",
|
||||||
|
"pass": window.location.protocol !== "http:",
|
||||||
|
"severe": true,
|
||||||
|
"message": "This can lead to data leaks. Please use a SSL certificate or expose your instance via a filestash domain"
|
||||||
|
}, {
|
||||||
|
"name_success": "Application is running as " + config.private.user.value,
|
||||||
|
"name_failure": "Application is running as root",
|
||||||
|
"pass": objectGet(config, ["private", "user", "value"]) !== "root",
|
||||||
|
"severe": true,
|
||||||
|
"message": "This is dangerous, you should use another user with less privileges"
|
||||||
|
}, {
|
||||||
|
"name_success": "Emacs is installed",
|
||||||
|
"name_failure": "Emacs is not installed",
|
||||||
|
"pass": objectGet(config, ["private", "emacs", "value"]),
|
||||||
|
"severe": false,
|
||||||
|
"message": "If you want to use all the org-mode features of Filestash, you need to install emacs"
|
||||||
|
}, {
|
||||||
|
"name_success": "Pdftotext is installed",
|
||||||
|
"name_failure": "Pdftotext is not installed",
|
||||||
|
"pass": objectGet(config, ["private", "pdftotext", "value"]),
|
||||||
|
"severe": false,
|
||||||
|
"message": "You won't be able to search through PDF documents without it"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}).catch((err) => {
|
||||||
|
notify.send(err && err.message, "error");
|
||||||
|
this.setState({busy: false});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
tunnelCall(){
|
||||||
renderStage(stage){
|
this.setState({busy: true});
|
||||||
if(stage === 0){
|
return Config.all().then((config) => {
|
||||||
return (
|
//this.setState({busy: false});
|
||||||
<div>
|
return objectGet(config, ["features", "server", "tunnel_url", "value"]);
|
||||||
<h2>You made it chief! { this.state.creating_password === true ? <Icon name="loading"/> : null}</h2>
|
});
|
||||||
<p>
|
|
||||||
Let's start by protecting the admin area with a password:
|
|
||||||
</p>
|
|
||||||
<form onSubmit={this.createPassword.bind(this)} style={{maxWidth: '350px'}}>
|
|
||||||
<Input type="password" placeholder="Create your admin password" defaultValue="" onChange={(e) => this.setState({password: e.target.value})} autoComplete="new-password"/>
|
|
||||||
<Button className="primary">Create Password</Button>
|
|
||||||
</form>
|
|
||||||
<style dangerouslySetInnerHTML={{__html: ".component_menu_sidebar{transform: translateX(-300px)}"}} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Welcome to the engine room</h2>
|
|
||||||
<p>
|
|
||||||
This is the place where you can configure filestash to your liking. Feel free to poke around. <br/>
|
|
||||||
You can come back by navigating at <a href="/admin">`{window.location.origin + "/admin"}`</a>. <br/>
|
|
||||||
Have fun!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
return (
|
return (
|
||||||
<div className="component_setup">
|
<div className="component_setup">
|
||||||
{ this.renderStage(this.state.stage) }
|
<MultiStepForm loading={this.state.busy}
|
||||||
|
onAdminPassword={this.onAdminPassword.bind(this)}
|
||||||
|
onExposeInstance={this.onExposeInstance.bind(this) }
|
||||||
|
summaryCall={this.summaryCall.bind(this)}
|
||||||
|
tunnelCall={this.tunnelCall.bind(this)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MultiStepForm extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
current: parseInt(window.location.hash.replace("#", "")) || 0,
|
||||||
|
answer_password: "",
|
||||||
|
has_answered_password: false,
|
||||||
|
answer_expose: "",
|
||||||
|
has_answered_expose: false,
|
||||||
|
deps: [],
|
||||||
|
redirect_uri: null,
|
||||||
|
working_message: "Working"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
if(this.state.current == 2){
|
||||||
|
this.fetchDependencies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdminPassword(e){
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onAdminPassword(this.state.answer_password, () => {
|
||||||
|
this.setState({current: 1, has_answered_password: true});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onExposeInstance(value, e){
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({answer_expose: value});
|
||||||
|
this.props.onExposeInstance(value, () => {
|
||||||
|
if(value === "tunnel"){
|
||||||
|
const waitForDomain = (count = 0) => {
|
||||||
|
return this.props.tunnelCall().then((url) => {
|
||||||
|
console.log("- config tunnel_url: %s - %d", url, count);
|
||||||
|
if(url && /\.filestash\.app$/.test(url) === true){
|
||||||
|
return Promise.resolve(url);
|
||||||
|
}
|
||||||
|
if(count > 10){
|
||||||
|
this.setState({working_message: "Building your domain"});
|
||||||
|
}else if(count > 30){
|
||||||
|
this.setState({working_message: "Processing ."+".".repeat(count % 3)});
|
||||||
|
}
|
||||||
|
if(count >= 60){
|
||||||
|
return Promise.reject({message: "Couldn't create a domain name"});
|
||||||
|
}
|
||||||
|
return new Promise((done) => window.setTimeout(done, 1000))
|
||||||
|
.then(() => waitForDomain(count + 1));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
waitForDomain().then((url) => {
|
||||||
|
// TODO
|
||||||
|
console.log("DONE: ", url, this.state);
|
||||||
|
this.setState({redirect_uri: url});
|
||||||
|
}).catch((err) => {
|
||||||
|
window.location.hash = "#2";
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({current: 2, has_answered_expose: true}, () => {
|
||||||
|
this.onStepChange(2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onStepChange(n){
|
||||||
|
this.setState({current: n});
|
||||||
|
if(n === 2){
|
||||||
|
this.fetchDependencies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDependencies() {
|
||||||
|
this.props.summaryCall().then((deps) => {
|
||||||
|
this.setState({deps: deps});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const hideMenu = <style dangerouslySetInnerHTML={{__html: ".component_menu_sidebar{transform: translateX(-300px)}"}} />;
|
||||||
|
if(this.state.current === 0) {
|
||||||
|
return (
|
||||||
|
<div id="step1" className="sharepage_component">
|
||||||
|
<FormStage navleft={false} navright={this.state.has_answered_password === true} current={this.state.current} onStepChange={this.onStepChange.bind(this)}>
|
||||||
|
Step 1/2: Secure your instance
|
||||||
|
</FormStage>
|
||||||
|
<ReactCSSTransitionGroup transitionName="stepper-form" transitionEnterTimeout={500} transitionAppearTimeout={500} transitionAppear={true} transitionEnter={true} transitionLeave={false}>
|
||||||
|
<div key={this.state.current}>
|
||||||
|
<p>Create your admin password: </p>
|
||||||
|
<form onSubmit={this.onAdminPassword.bind(this)}>
|
||||||
|
<Input ref="$input" type="password" placeholder="Password" value={this.state.answer_password} onChange={(e) => this.setState({answer_password: e.target.value})}/>
|
||||||
|
<Button theme="transparent">
|
||||||
|
<Icon name={this.props.loading ? "loading" : "arrow_right"}/>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</ReactCSSTransitionGroup>
|
||||||
|
{hideMenu}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if(this.state.current === 1) {
|
||||||
|
return (
|
||||||
|
<div id="step2">
|
||||||
|
<FormStage navleft={true} navright={this.state.has_answered_expose} current={this.state.current} onStepChange={this.onStepChange.bind(this)}>
|
||||||
|
Step 2/2: Expose your instance to the internet ?
|
||||||
|
</FormStage>
|
||||||
|
<ReactCSSTransitionGroup transitionName="stepper-form" transitionEnterTimeout={500} transitionAppearTimeout={500} transitionAppear={true} transitionEnter={true} transitionLeave={false}>
|
||||||
|
<div key={this.state.current}>
|
||||||
|
<NgIf cond={this.state.redirect_uri !== null}>
|
||||||
|
<div style={{textAlign: "center"}}>
|
||||||
|
Your instance is available at <a href={this.state.redirect_uri}>{this.state.redirect_uri}</a>.<br/>
|
||||||
|
You will be redirected in <Countdown max={9} onZero={() => window.location.href = this.state.redirect_uri} /> seconds
|
||||||
|
</div>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={!this.props.loading && this.state.redirect_uri === null}>
|
||||||
|
<form onSubmit={this.onExposeInstance.bind(this, "skip")}>
|
||||||
|
<label className={this.state.answer_expose === "nothing" ? "active" : ""}>
|
||||||
|
<input type="radio" name="expose" value="nothing" checked={this.state.answer_expose === "nothing"} onChange={this.onExposeInstance.bind(this, "nothing")}/>
|
||||||
|
No, don't expose anything to the internet
|
||||||
|
</label>
|
||||||
|
<label className={this.state.answer_expose === "tunnel" ? "active" : ""}>
|
||||||
|
<input type="radio" name="expose" value="tunnel" checked={this.state.answer_expose === "tunnel"} onChange={this.onExposeInstance.bind(this, "tunnel")}/>
|
||||||
|
Yes, and make it available via a filestash subdomain - eg: https://user-me.filestash.app
|
||||||
|
</label>
|
||||||
|
<label className={this.state.answer_expose === "skip" ? "active" : ""}>
|
||||||
|
<input type="radio" name="expose" value="skip" checked={this.state.answer_expose === "skip"} onChange={this.onExposeInstance.bind(this, "skip")}/>
|
||||||
|
Skip if you're a wizard when it comes to SSL certificates and port forwarding
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={!!this.props.loading && this.state.redirect_uri === null}>
|
||||||
|
<Loader/>
|
||||||
|
<div style={{textAlign: "center"}}>{this.state.working_message}</div>
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
</ReactCSSTransitionGroup>
|
||||||
|
{hideMenu}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if(this.state.current === 2) {
|
||||||
|
return (
|
||||||
|
<div id="step3">
|
||||||
|
<FormStage navleft={true} navright={false} current={this.state.current} onStepChange={this.onStepChange.bind(this)}>
|
||||||
|
Summary
|
||||||
|
</FormStage>
|
||||||
|
<ReactCSSTransitionGroup transitionName="stepper-form" transitionEnterTimeout={500} transitionAppearTimeout={500} transitionAppear={true} transitionEnter={true} transitionLeave={false}>
|
||||||
|
<div key={this.state.current}>
|
||||||
|
<NgIf cond={!!this.props.loading}>
|
||||||
|
<Loader/>
|
||||||
|
<div style={{textAlign: "center"}}>Verifying</div>
|
||||||
|
</NgIf>
|
||||||
|
<NgIf cond={!this.props.loading}>
|
||||||
|
{
|
||||||
|
this.state.deps.map((dep, idx) => {
|
||||||
|
return (
|
||||||
|
<div className={"component_dependency_installed" + (dep.pass ? " yes" : " no") + (dep.severe ? " severe" : "")} key={idx}>
|
||||||
|
<span>{dep.pass ? dep.name_success : dep.name_failure}</span>{dep.pass ? null : ": " + dep.message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</NgIf>
|
||||||
|
</div>
|
||||||
|
</ReactCSSTransitionGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormStage = (props) => {
|
||||||
|
return (
|
||||||
|
<h4>
|
||||||
|
{ props.navleft === true ? <Icon name="arrow_left" onClick={() => props.onStepChange(props.current - 1)}/> : null}
|
||||||
|
{ props.children }
|
||||||
|
{ props.navright === true ? <Icon name="arrow_right" onClick={() => props.onStepChange(props.current + 1)}/> : null }
|
||||||
|
</h4>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Countdown extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = { count: props.max };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
this.timeout = window.setInterval(() => {
|
||||||
|
if(this.state.count - 1 >= 0){
|
||||||
|
this.setState({count: this.state.count - 1}, () => {
|
||||||
|
if(this.state.count === 0) this.props.onZero();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
window.clearInterval(this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return(
|
||||||
|
<span>{this.state.count}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectGet(obj, paths){
|
||||||
|
let value = obj;
|
||||||
|
for(let i=0; i<paths.length; i++){
|
||||||
|
if(typeof value !== "object") return null;
|
||||||
|
value = value[paths[i]];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,19 +1,75 @@
|
|||||||
.component_setup{
|
.component_setup{
|
||||||
transform: none!important; // transition and fixed posiiton doesn't cohabit well so we have to resort
|
transform: none!important; // transition and fixed position doesn't cohabit well so we have to resort
|
||||||
// to remove animation on this page to preserve the layout
|
// to remove animation on this page to preserve the layout
|
||||||
text-align: justify;
|
|
||||||
|
|
||||||
button.completed{
|
h4{
|
||||||
position: fixed;
|
user-select: none;
|
||||||
bottom: 0;
|
text-align: center;
|
||||||
right: 0;
|
font-size: 1.4em;
|
||||||
width: 150px;
|
font-weight: 500;
|
||||||
padding: 15px;
|
padding: 20px 0 0px 0;
|
||||||
box-sizing: border-box;
|
.component_icon{
|
||||||
border-top-left-radius: 10px;
|
vertical-align: text-top;
|
||||||
font-size: 1.1em;
|
width: 1.3em;
|
||||||
color: var(--emphasis);
|
cursor: pointer;
|
||||||
|
&[alt="loading"]{ opacity: 0; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#step1{
|
||||||
|
p {
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
form{
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#step2{
|
||||||
|
label{
|
||||||
|
display: block;
|
||||||
|
background: #f2f3f5;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
input[type="radio"]{ display: none; }
|
||||||
|
transition: background 0.05s ease;
|
||||||
|
&:hover, &.active{ background: var(--emphasis-primary); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#step3{
|
||||||
|
.component_dependency_installed{
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--emphasis);
|
||||||
|
&.yes{
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
&.no{
|
||||||
|
background: var(--primary);
|
||||||
|
&.severe{
|
||||||
|
background: var(--error);
|
||||||
|
span{
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepper-form-appear, .stepper-form-enter{
|
||||||
|
transition-delay: 0.3s;
|
||||||
|
transform: scale(1.02);
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
&.stepper-form-appear-active, &.stepper-form-enter-active{
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.component_icon{
|
.component_icon{
|
||||||
width: 30px;
|
width: 30px;
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"strings"
|
"strings"
|
||||||
@ -494,7 +496,34 @@ func (this Configuration) Interface() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this Configuration) MarshalJSON() ([]byte, error) {
|
func (this Configuration) MarshalJSON() ([]byte, error) {
|
||||||
|
form := this.form
|
||||||
|
form = append(form, Form{
|
||||||
|
Title: "private",
|
||||||
|
Elmnts: []FormElement{
|
||||||
|
FormElement{Name: "user", Type: "boolean", ReadOnly: true, Value: func() string{
|
||||||
|
if u, err := user.Current(); err == nil {
|
||||||
|
if u.Username != "" {
|
||||||
|
return u.Username
|
||||||
|
}
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
return "n/a"
|
||||||
|
}()},
|
||||||
|
FormElement{Name: "emacs", Type: "boolean", ReadOnly: true, Value: func() bool {
|
||||||
|
if _, err := exec.LookPath("emacs"); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}()},
|
||||||
|
FormElement{Name: "pdftotext", Type: "boolean", ReadOnly: true, Value: func() bool {
|
||||||
|
if _, err := exec.LookPath("pdftotext"); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}()},
|
||||||
|
},
|
||||||
|
})
|
||||||
return Form{
|
return Form{
|
||||||
Form: this.form,
|
Form: form,
|
||||||
}.MarshalJSON()
|
}.MarshalJSON()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,6 @@ func AdminSessionGet(ctx App, res http.ResponseWriter, req *http.Request) {
|
|||||||
SendSuccessResult(res, false)
|
SendSuccessResult(res, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SendSuccessResult(res, true)
|
SendSuccessResult(res, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,7 @@ func init() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
Config.Get("features.server.tunnel_url").Set(nil)
|
||||||
if tunnel_enable() == false {
|
if tunnel_enable() == false {
|
||||||
startTunnel := false
|
startTunnel := false
|
||||||
for {
|
for {
|
||||||
@ -90,9 +91,9 @@ func init() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
req.Header.Add("X-Machine-ID", GenerateMachineID())
|
req.Header.Add("X-Machine-ID", GenerateMachineID())
|
||||||
res, err := HTTPClient.Do(req)
|
res, err := HTTP.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Log.Info("[tunnel] registration_failure %s", err.Error())
|
Log.Info("[tunnel] registration_error %s", err.Error())
|
||||||
time.Sleep(backoff())
|
time.Sleep(backoff())
|
||||||
continue
|
continue
|
||||||
} else if res.StatusCode != http.StatusOK {
|
} else if res.StatusCode != http.StatusOK {
|
||||||
|
|||||||
Reference in New Issue
Block a user