Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a50409bdca | |||
| 60a722e6cc | |||
| 4d6cafa589 | |||
| 63e557430b | |||
| 04acb4423d | |||
| ea813acf4c | |||
| b1198dfdbf | |||
| 4342de3747 | |||
| ef8bc7e7e9 | |||
| e18e401f6b | |||
| ab998c47e8 | |||
| 9fb830b2e1 | |||
| 415c3f90a1 | |||
| 60c8ff7fb1 | |||
| 037816313c | |||
| 3d285d1ac6 | |||
| 135338786f | |||
| 661f1fce31 | |||
| 03664ef784 | |||
| d2111ef2b6 |
@@ -805,7 +805,3 @@
|
||||
* (mail) Set maximum email size to 25MB
|
||||
* Remove SimpleAuth addon
|
||||
|
||||
[0.107.0]
|
||||
* Support CSP for webinterface and OAuth views
|
||||
* (mail) Fix issue where Cloudron is only used to send emails
|
||||
|
||||
|
||||
@@ -53,10 +53,6 @@ Cloudron has a built-in firewall and ports are opened and closed dynamically, as
|
||||
apps are installed, re-configured or removed. For this reason, be sure to open all TCP and
|
||||
UDP traffic to the server and leave the traffic management to the Cloudron.
|
||||
|
||||
### Kimsufi
|
||||
|
||||
Be sure to check the "use the distribution kernel" checkbox in the personalized installation mode.
|
||||
|
||||
### Linode
|
||||
|
||||
Since Linode does not manage SSH keys, be sure to add the public key to
|
||||
|
||||
@@ -341,18 +341,6 @@ beyond it's control, Cloudron admins will get a notification about it.
|
||||
All the operations listed in this manual like installing app, configuring users and groups, are
|
||||
completely programmable with a [REST API](/references/api.html).
|
||||
|
||||
# OAuth Provider
|
||||
|
||||
Cloudron is an OAuth 2.0 provider. To integrate Cloudron login into an external application, create
|
||||
an OAuth application under `API Access`.
|
||||
|
||||
You can use the following OAuth URLs to add Cloudron in the external app:
|
||||
```
|
||||
authorizationURL: https://my.<domain>/api/v1/oauth/dialog/authorize
|
||||
|
||||
tokenURL: https://my.<domain>/api/v1/oauth/token
|
||||
```
|
||||
|
||||
# Moving to a larger Cloudron
|
||||
|
||||
When using a Cloudron from cloudron.io, it is easy to migrate your apps and data to a bigger server.
|
||||
|
||||
@@ -42,12 +42,12 @@ Creating an application for Cloudron can be summarized as follows:
|
||||
1. Create a web application using any language/framework. This web application must run a HTTP server
|
||||
and can optionally provide other services using custom protocols (like git, ssh, TCP etc).
|
||||
|
||||
2. Create a [Dockerfile](http://docs.docker.com/engine/reference/builder/) that specifies how to create
|
||||
2. Create a [Dockerfile](http://docs.docker.com/engine/reference/builder/) that specifies how to create
|
||||
an application ```image```. An ```image``` is essentially a bundle of the application source code
|
||||
and it's dependencies.
|
||||
|
||||
3. Create a [CloudronManifest.json](/references/manifest.html) file that provides essential information
|
||||
about the app. This includes information required for the Cloudron Store like title, version, icon and
|
||||
about the app. This includes information required for the Cloudron Store like title, version, icon and
|
||||
runtime requirements like `addons`.
|
||||
|
||||
## Simple Web application
|
||||
@@ -79,7 +79,7 @@ FROM cloudron/base:0.10.0
|
||||
|
||||
ADD server.js /app/code/server.js
|
||||
|
||||
CMD [ "/usr/local/node-6.9.5/bin/node", "/app/code/server.js" ]
|
||||
CMD [ "/usr/local/node-0.12.7/bin/node", "/app/code/server.js" ]
|
||||
```
|
||||
|
||||
The `FROM` command specifies that we want to start off with Cloudron's [base image](/references/baseimage.html).
|
||||
@@ -90,7 +90,7 @@ While this example only copies a single file, the ADD command can be used to cop
|
||||
See the [Dockerfile](https://docs.docker.com/reference/builder/#add) documentation for more details.
|
||||
|
||||
The `CMD` command specifies how to run the server. There are multiple versions of node available under `/usr/local`. We
|
||||
choose node v6.9.5 for our app.
|
||||
choose node v0.12.7 for our app.
|
||||
|
||||
## CloudronManifest.json
|
||||
|
||||
@@ -176,7 +176,7 @@ Step 0 : FROM cloudron/base:0.10.0
|
||||
Step 1 : ADD server.js /app/code
|
||||
---> b09b97ecdfbc
|
||||
Removing intermediate container 03c1e1f77acb
|
||||
Step 2 : CMD /usr/local/node-6.9.5/bin/node /app/code/main.js
|
||||
Step 2 : CMD /usr/local/node-0.12.7/bin/node /app/code/main.js
|
||||
---> Running in 370f59d87ab2
|
||||
---> 53b51eabcb89
|
||||
Removing intermediate container 370f59d87ab2
|
||||
@@ -335,15 +335,13 @@ File `tutorial/Dockerfile`
|
||||
```dockerfile
|
||||
FROM cloudron/base:0.10.0
|
||||
|
||||
ENV PATH /usr/local/node-6.9.5/bin:$PATH
|
||||
|
||||
ADD server.js /app/code/server.js
|
||||
ADD package.json /app/code/package.json
|
||||
|
||||
WORKDIR /app/code
|
||||
RUN npm install --production
|
||||
|
||||
CMD [ "node", "/app/code/server.js" ]
|
||||
CMD [ "/usr/local/node-0.12.7/bin/node", "/app/code/server.js" ]
|
||||
```
|
||||
|
||||
Notice the new `RUN` command which installs the node module dependencies in package.json using `npm install`.
|
||||
|
||||
+14
-17
@@ -2,18 +2,17 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
del = require('del'),
|
||||
ejs = require('gulp-ejs'),
|
||||
var ejs = require('gulp-ejs'),
|
||||
gulp = require('gulp'),
|
||||
sass = require('gulp-sass'),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
del = require('del'),
|
||||
concat = require('gulp-concat'),
|
||||
uglify = require('gulp-uglify'),
|
||||
url = require('url');
|
||||
serve = require('gulp-serve'),
|
||||
sass = require('gulp-sass'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
argv = require('yargs').argv;
|
||||
|
||||
gulp.task('3rdparty', function () {
|
||||
gulp.src([
|
||||
@@ -55,16 +54,14 @@ gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-update'], function (
|
||||
var oauth = {
|
||||
clientId: argv.clientId || 'cid-webadmin',
|
||||
clientSecret: argv.clientSecret || 'unused',
|
||||
apiOrigin: argv.apiOrigin || '',
|
||||
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
|
||||
apiOrigin: argv.apiOrigin || ''
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log('Using OAuth credentials:');
|
||||
console.log(' ClientId: %s', oauth.clientId);
|
||||
console.log(' ClientSecret: %s', oauth.clientSecret);
|
||||
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
|
||||
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
|
||||
console.log(' ClientId: %s', oauth.clientId);
|
||||
console.log(' ClientSecret: %s', oauth.clientSecret);
|
||||
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
|
||||
console.log();
|
||||
|
||||
|
||||
@@ -143,7 +140,7 @@ gulp.task('js-update', function () {
|
||||
// --------------
|
||||
|
||||
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
|
||||
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
|
||||
return gulp.src('webadmin/src/*.html').pipe(gulp.dest('webadmin/dist'));
|
||||
});
|
||||
|
||||
gulp.task('html-update', function () {
|
||||
|
||||
@@ -15,7 +15,7 @@ fi
|
||||
# change this to a hash when we make a upgrade release
|
||||
readonly LOG_FILE="/var/log/cloudron-setup.log"
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
|
||||
readonly MINIMUM_DISK_SIZE_GB="19" # this is the size of "/" and required to fit in docker images 19 is a safe bet for different reporting on 20GB min
|
||||
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
@@ -23,8 +23,8 @@ readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max
|
||||
# copied from cloudron-resize-fs.sh
|
||||
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mountpoint -x $d)" ] && echo $d && break; done)"
|
||||
readonly disk_size_bytes=$(LC_ALL=C df | grep "${disk_device}" | awk '{ printf $2 }')
|
||||
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
|
||||
readonly disk_size_bytes=$(LC_ALL=C fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }')
|
||||
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024/1024))
|
||||
|
||||
# verify the system has minimum requirements met
|
||||
if [[ "${physical_memory}" -lt "${MINIMUM_MEMORY}" ]]; then
|
||||
|
||||
@@ -16,8 +16,8 @@ existing_swap=$(cat /proc/meminfo | grep SwapTotal | awk '{ printf "%.0f", $2/10
|
||||
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly swap_size=$((${physical_memory} - ${existing_swap})) # if you change this, fix enoughResourcesAvailable() in client.js
|
||||
readonly app_count=$((${physical_memory} / 200)) # estimated app count
|
||||
readonly disk_size_bytes=$(LC_ALL=C df | grep "${disk_device}" | awk '{ printf $2 }')
|
||||
readonly disk_size=$((${disk_size_bytes}/1024))
|
||||
readonly disk_size_bytes=$(LC_ALL=C fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }') # can't rely on fdisk human readable units, using bytes instead
|
||||
readonly disk_size=$((${disk_size_bytes}/1024/1024))
|
||||
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code, data and tmp
|
||||
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
|
||||
|
||||
|
||||
@@ -32,19 +32,14 @@ server {
|
||||
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
|
||||
add_header X-Frame-Options "<%= xFrameOptions %>";
|
||||
proxy_hide_header X-Frame-Options;
|
||||
|
||||
# https://github.com/twitter/secureheaders
|
||||
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
|
||||
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
proxy_hide_header X-XSS-Protection;
|
||||
add_header X-Download-Options "noopen";
|
||||
proxy_hide_header X-Download-Options;
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
proxy_hide_header X-Content-Type-Options;
|
||||
add_header X-Permitted-Cross-Domain-Policies "none";
|
||||
proxy_hide_header X-Permitted-Cross-Domain-Policies;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_intercept_errors on;
|
||||
@@ -139,3 +134,4 @@ server {
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ exports = module.exports = {
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.16.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.12.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.30.5' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.30.3' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
|
||||
listAliases: listAliases,
|
||||
listMailboxes: listMailboxes,
|
||||
// listGroups: listGroups, // this is beyond my SQL skillz
|
||||
|
||||
getMailbox: getMailbox,
|
||||
getGroup: getGroup,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<% include header %>
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<script>
|
||||
|
||||
'use strict';
|
||||
|
||||
// very basic angular app
|
||||
var app = angular.module('Application', []);
|
||||
app.controller('Controller', ['$scope', function ($scope) {
|
||||
$scope.success = <%= success %>;
|
||||
$scope.error = '<%= error %>';
|
||||
}]);
|
||||
|
||||
</script>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller" ng-cloak>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<br/>
|
||||
<h4 ng-hide="success">Hello there, welcome to <%= cloudronName %>.</h4>
|
||||
<h2 ng-hide="success">Sign up with your email address.</h2>
|
||||
<h3 ng-show="success">You have received an email invitation to this Cloudron to finish the signup.</h3>
|
||||
<br/><br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3" ng-show="!success">
|
||||
<form action="/api/v1/session/account/create" method="post" name="createForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (createForm.email.$dirty && createForm.email.$invalid) || (!createForm.email.$dirty && error) }">
|
||||
<label class="control-label" for="inputEmail">Email</label>
|
||||
<input type="email" class="form-control" id="inputEmail" ng-model="email" name="email" autofocus required>
|
||||
<div class="control-label" ng-show="(createForm.email.$dirty && createForm.email.$invalid) || (!createForm.email.$dirty && error)">
|
||||
<small ng-show="createForm.email.$dirty && createForm.email.$invalid">Must be a valid email address</small>
|
||||
<small ng-show="!createForm.email.$dirty && error">{{ error }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="createForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
@@ -32,19 +32,19 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
<center><p class="has-error"><%= error %></p></center>
|
||||
|
||||
<% if (user && user.username) { %>
|
||||
<div class="form-group"">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
|
||||
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
|
||||
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
@@ -55,18 +55,18 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
|
||||
<label class="control-label">New Password</label>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
<label class="control-label">Repeat Password</label>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self'; img-src 'self'" />
|
||||
|
||||
<title> <%= title %> </title>
|
||||
|
||||
|
||||
@@ -26,17 +26,17 @@ app.controller('Controller', [function () {}]);
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
|
||||
@@ -289,7 +289,6 @@ function startMail(callback) {
|
||||
--net-alias mail \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--env ENABLE_MDA=${mailConfig.enabled} \
|
||||
-v "${dataDir}/mail:/app/data" \
|
||||
-v "${dataDir}/addons/mail:/etc/mail" \
|
||||
${ports} \
|
||||
|
||||
@@ -11,6 +11,7 @@ var appdb = require('../appdb'),
|
||||
DatabaseError = require('../databaseerror'),
|
||||
debug = require('debug')('box:routes/oauth2'),
|
||||
eventlog = require('../eventlog.js'),
|
||||
generatePassword = require('../password.js').generate,
|
||||
hat = require('hat'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
middleware = require('../middleware/index.js'),
|
||||
@@ -357,6 +358,87 @@ function accountSetup(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
// -> POST /api/v1/session/account/setup
|
||||
function accountSetup(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'Missing username'));
|
||||
if (typeof req.body.displayName !== 'string') return next(new HttpError(400, 'Missing displayName'));
|
||||
|
||||
debug('accountSetup: with token %s.', req.body.resetToken);
|
||||
|
||||
user.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||
if (error) return sendError(req, res, 'Invalid Reset Token');
|
||||
|
||||
var data = _.pick(req.body, 'username', 'displayName');
|
||||
user.update(userObject.id, data, auditSource(req), function (error) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
|
||||
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
|
||||
if (error && error.reason === UserError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
userObject.username = req.body.username;
|
||||
userObject.displayName = req.body.displayName;
|
||||
|
||||
// setPassword clears the resetToken
|
||||
user.setPassword(userObject.id, req.body.password, function (error, result) {
|
||||
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
|
||||
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderAccountCreateSite(res, req, error, success) {
|
||||
renderTemplate(res, 'account_create', {
|
||||
error: error,
|
||||
success: !!success,
|
||||
csrf: req.csrfToken(),
|
||||
title: 'Account Create'
|
||||
});
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/account/create.html
|
||||
function accountCreateSite(req, res, next) {
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (!enabled) return sendError(req, res, 'User creation is not allowed on this Cloudron');
|
||||
|
||||
renderAccountCreateSite(res, req, '', '');
|
||||
});
|
||||
}
|
||||
|
||||
// -> POST /api/v1/session/account/create
|
||||
function accountCreate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
|
||||
|
||||
debug('accountCreate: with email %s.', req.body.email);
|
||||
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (!enabled) return sendError(req, res, 'User signup is not allowed on this Cloudron');
|
||||
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
var auditSource = { ip: ip, username: req.body.email, userId: null };
|
||||
|
||||
user.create(null, generatePassword(), req.body.email, '', auditSource, { sendInvite: true }, function (error, result) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountCreateSite(res, req, 'User with this email address already exists');
|
||||
if (error) return sendError(req, res, 'Internal Error');
|
||||
|
||||
debug('accountCreate: success for email %s now with id %s', req.body.remail, result.id);
|
||||
|
||||
renderAccountCreateSite(res, req, '', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/password/reset.html
|
||||
function passwordResetSite(req, res, next) {
|
||||
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
|
||||
@@ -555,6 +637,8 @@ exports = module.exports = {
|
||||
passwordReset: passwordReset,
|
||||
accountSetupSite: accountSetupSite,
|
||||
accountSetup: accountSetup,
|
||||
accountCreateSite: accountCreateSite,
|
||||
accountCreate: accountCreate,
|
||||
authorization: authorization,
|
||||
token: token,
|
||||
validateRequestedScopes: validateRequestedScopes,
|
||||
|
||||
@@ -27,6 +27,9 @@ exports = module.exports = {
|
||||
getAppstoreConfig: getAppstoreConfig,
|
||||
setAppstoreConfig: setAppstoreConfig,
|
||||
|
||||
getOpenRegistration: getOpenRegistration,
|
||||
setOpenRegistration: setOpenRegistration,
|
||||
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
setAdminCertificate: setAdminCertificate
|
||||
};
|
||||
@@ -234,6 +237,27 @@ function setAppstoreConfig(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getOpenRegistration(req, res, next) {
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { enabled: enabled }));
|
||||
});
|
||||
}
|
||||
|
||||
function setOpenRegistration(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
|
||||
|
||||
settings.setOpenRegistration(req.body.enabled, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
// default fallback cert
|
||||
function setFallbackCertificate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
@@ -770,4 +770,45 @@ describe('Settings API', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('open_registration', function () {
|
||||
it('get open_registration succeeds without being set', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/open_registration')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.enabled).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without data', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/open_registration')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/open_registration')
|
||||
.query({ access_token: token })
|
||||
.send({ enabled: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/open_registration')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.enabled).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
-1
@@ -148,6 +148,8 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/session/password/reset', csrf, routes.oauth2.passwordReset);
|
||||
router.get ('/api/v1/session/account/setup.html', csrf, routes.oauth2.accountSetupSite);
|
||||
router.post('/api/v1/session/account/setup', csrf, routes.oauth2.accountSetup);
|
||||
router.get ('/api/v1/session/account/create.html', csrf, routes.oauth2.accountCreateSite);
|
||||
router.post('/api/v1/session/account/create', csrf, routes.oauth2.accountCreate);
|
||||
|
||||
// oauth2 routes
|
||||
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
|
||||
@@ -194,7 +196,6 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.setBackupConfig);
|
||||
router.post('/api/v1/settings/certificate', settingsScope, routes.user.requireAdmin, routes.settings.setFallbackCertificate);
|
||||
|
||||
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.user.requireAdmin, routes.settings.setAdminCertificate);
|
||||
router.get ('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.getTimeZone);
|
||||
router.post('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.setTimeZone);
|
||||
@@ -202,6 +203,8 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.setAppstoreConfig);
|
||||
router.get ('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.getMailConfig);
|
||||
router.post('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.setMailConfig);
|
||||
router.get ('/api/v1/settings/open_registration', settingsScope, routes.user.requireAdmin, routes.settings.getOpenRegistration);
|
||||
router.post('/api/v1/settings/open_registration', settingsScope, routes.user.requireAdmin, routes.settings.setOpenRegistration);
|
||||
|
||||
// eventlog route
|
||||
router.get('/api/v1/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
|
||||
|
||||
@@ -44,6 +44,9 @@ exports = module.exports = {
|
||||
getMailConfig: getMailConfig,
|
||||
setMailConfig: setMailConfig,
|
||||
|
||||
getOpenRegistration: getOpenRegistration,
|
||||
setOpenRegistration: setOpenRegistration,
|
||||
|
||||
getDefaultSync: getDefaultSync,
|
||||
getAll: getAll,
|
||||
|
||||
@@ -58,6 +61,7 @@ exports = module.exports = {
|
||||
UPDATE_CONFIG_KEY: 'update_config',
|
||||
APPSTORE_CONFIG_KEY: 'appstore_config',
|
||||
MAIL_CONFIG_KEY: 'mail_config',
|
||||
OPEN_REGISTRATION_KEY: 'open_registration',
|
||||
|
||||
events: null
|
||||
};
|
||||
@@ -102,6 +106,7 @@ var gDefaults = (function () {
|
||||
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
|
||||
result[exports.APPSTORE_CONFIG_KEY] = {};
|
||||
result[exports.MAIL_CONFIG_KEY] = { enabled: false };
|
||||
result[exports.OPEN_REGISTRATION_KEY] = false;
|
||||
|
||||
return result;
|
||||
})();
|
||||
@@ -645,6 +650,31 @@ function setMailConfig(mailConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getOpenRegistration(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.OPEN_REGISTRATION_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.OPEN_REGISTRATION_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
// settingsdb holds string values only
|
||||
callback(null, !!value);
|
||||
});
|
||||
}
|
||||
|
||||
function setOpenRegistration(enabled, callback) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.set(exports.OPEN_REGISTRATION_KEY, enabled ? 'enabled' : '', function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.OPEN_REGISTRATION_KEY, enabled);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getAppstoreConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
@@ -188,5 +188,28 @@ describe('Settings', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get open registration default value', function (done) {
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
expect(enabled).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set open registration', function (done) {
|
||||
settings.setOpenRegistration(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get open registration', function (done) {
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
expect(enabled).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron App Error </title>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron Error </title>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src *;" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Cloudron </title>
|
||||
|
||||
@@ -439,6 +439,20 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setOpenRegistration = function (enabled, callback) {
|
||||
post('/api/v1/settings/open_registration', { enabled: enabled }).success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getOpenRegistration = function (callback) {
|
||||
get('/api/v1/settings/open_registration').success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null, data.enabled);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getEmailStatus = function (callback) {
|
||||
get('/api/v1/settings/email_status').success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<title> Cloudron OAuth Callback </title>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>" />
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron </title>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron Admin Setup </title>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron Setup </title>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"/>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' *.cloudron.io <%= apiOriginHostname %>" />
|
||||
|
||||
<title> Cloudron </title>
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appUpdateForm.password.$dirty && appUpdate.error.password) || (appUpdateForm.password.$dirty && appUpdateForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputUpdatePassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" ng-maxlength="30" ng-minlength="8" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appUpdateForm.$invalid || busy"/>
|
||||
</form>
|
||||
@@ -412,7 +412,7 @@
|
||||
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
|
||||
<div ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure'">
|
||||
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-pencil scale"></i></a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.customDomainId.$invalid }" uib-tooltip="{{ config.provider === 'caas' ? '' : 'Changing the domain is not yet supported' }}">
|
||||
<label class="control-label" for="customDomainId">Domain name</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" check-tld placeholder="example.com" required autofocus>
|
||||
<p> <span ng-show="dnsCredentialsForm.customDomainId.$error.invalidSubdomain && dnsCredentials.customDomain !== config.fqdn" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
|
||||
<p> <span ng-show="dnsCredentialsForm.customDomainId.$error.invalidSubdomain" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -28,15 +28,19 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'route53'">
|
||||
<br/>
|
||||
<p>This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</p>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<label class="control-label" for="dnsCredentialsDigitalOceanToken">DigitalOcean token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" id="dnsCredentialsDigitalOceanToken" name="digitalOceanToken" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'digitalocean'">
|
||||
<br/>
|
||||
<p>This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-if="config.fqdn !== dnsCredentials.customDomain && !dnsCredentialsForm.customDomainId.$invalid">
|
||||
<label class="control-label" for="dnsCredentialsPassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="dnsCredentials.password" id="dnsCredentialsPassword" name="password" ng-disabled="dnsCredentials.busy" required>
|
||||
</div>
|
||||
@@ -45,20 +49,14 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'route53'">
|
||||
This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.
|
||||
</p>
|
||||
|
||||
<!-- Wildcard -->
|
||||
<p ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
Setup <i>A</i> records for <b>*.{{ dnsCredentials.customDomain || 'example.com' }}</b> and <b>{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP.
|
||||
Setup <i>A</i> records for <b>*.{{ dnsCredentials.domain || 'example.com' }}</b> and <b>{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.
|
||||
</p>
|
||||
|
||||
<!-- Manual -->
|
||||
<p ng-show="dnsCredentials.provider === 'manual'">
|
||||
Setup an <i>A</i> record for <b>my.{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
Setup an <i>A</i> record for <b>my.{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
|
||||
@@ -184,8 +184,7 @@ angular.module('Application').controller('CertsController', ['$scope', '$locatio
|
||||
$scope.showChangeDnsCredentials = function () {
|
||||
dnsCredentialsReset();
|
||||
|
||||
// clear the input box for non-custom domain
|
||||
$scope.dnsCredentials.customDomain = $scope.config.isCustomDomain ? $scope.config.fqdn : '';
|
||||
$scope.dnsCredentials.customDomain = $scope.config.fqdn;
|
||||
$scope.dnsCredentials.accessKeyId = $scope.dnsConfig.accessKeyId;
|
||||
$scope.dnsCredentials.secretAccessKey = $scope.dnsConfig.secretAccessKey;
|
||||
$scope.dnsCredentials.digitalOceanToken = $scope.dnsConfig.provider === 'digitalocean' ? $scope.dnsConfig.token : '';
|
||||
|
||||
@@ -450,5 +450,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>User Registration</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
By default the Cloudron only allows admins to invite other users.
|
||||
You may enable user registration, allowing users to signup without such an invite.
|
||||
</p>
|
||||
<p ng-show="openRegistrationEnabled">
|
||||
The user signup link is: <a ng-href="{{ signupLink }}" target="_blank">{{ signupLink }}</a>
|
||||
</p>
|
||||
<br/>
|
||||
<button class="btn btn-primary pull-right" ng-click="toggleOpenRegistration()">{{ openRegistrationEnabled ? 'Disable user registration' : 'Enable user registration' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offset the footer -->
|
||||
<br/><br/>
|
||||
|
||||
@@ -6,6 +6,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
$scope.client = Client;
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.openRegistrationEnabled = false;
|
||||
$scope.signupLink = '';
|
||||
$scope.backupConfig = {};
|
||||
$scope.dnsConfig = {};
|
||||
$scope.outboundPort25 = {};
|
||||
@@ -507,6 +509,16 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
});
|
||||
}
|
||||
|
||||
function getOpenRegistration() {
|
||||
Client.getOpenRegistration(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.openRegistrationEnabled = enabled;
|
||||
|
||||
$scope.signupLink = window.location.origin + '/api/v1/session/account/create.html';
|
||||
});
|
||||
}
|
||||
|
||||
function showExpectedDnsRecords(callback) {
|
||||
callback = callback || function (error) { if (error) console.error(error); };
|
||||
|
||||
@@ -613,12 +625,20 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleOpenRegistration = function () {
|
||||
Client.setOpenRegistration(!$scope.openRegistrationEnabled, function (error) {
|
||||
if (error) return console.error(error);
|
||||
$scope.openRegistrationEnabled = !$scope.openRegistrationEnabled;
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
fetchBackups();
|
||||
getMailConfig();
|
||||
getBackupConfig();
|
||||
getDnsConfig();
|
||||
getAutoupdatePattern();
|
||||
getOpenRegistration();
|
||||
|
||||
if ($scope.config.provider === 'caas') {
|
||||
getPlans();
|
||||
|
||||
@@ -4,25 +4,18 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add OAuth Client</h4>
|
||||
<h4 class="modal-title">Add API Client</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.name.$dirty && clientAddForm.name.$invalid) || (!clientAddForm.name.$dirty && clientAdd.error.name) }">
|
||||
<label class="control-label">Application name</label>
|
||||
<label class="control-label">Name</label>
|
||||
<div class="control-label" ng-show="(!clientAddForm.name.$dirty && clientAdd.error.name) || (clientAddForm.name.$dirty && clientAddForm.name.$invalid)">
|
||||
<small ng-show="clientAddForm.name.$error.required">A name is required</small>
|
||||
<small ng-show="!clientAddForm.name.$dirty && clientAdd.error.name">{{ clientAdd.error.name }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="clientAdd.name" name="name" id="clientAddName" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.redirectURI.$dirty && clientAddForm.redirectURI.$invalid) || (!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI) }">
|
||||
<label class="control-label">Authorization callback URL</label>
|
||||
<div class="control-label" ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">
|
||||
<small ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">{{ clientAdd.error.redirectURI }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="clientAdd.redirectURI" name="redirectURI" id="clientAddRedirectURI" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.scope.$dirty && clientAddForm.scope.$invalid) || (!clientAddForm.scope.$dirty && clientAdd.error.scope) }">
|
||||
<label class="control-label">Scope</label>
|
||||
<div class="control-label" ng-show="(!clientAddForm.scope.$dirty && clientAdd.error.scope) || (clientAddForm.scope.$dirty && clientAddForm.scope.$invalid)">
|
||||
@@ -31,12 +24,19 @@
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="clientAdd.scope" name="scope" id="clientAddScope" placeholder="Specify any number of scope separated by a comma ','" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.redirectURI.$dirty && clientAddForm.redirectURI.$invalid) || (!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI) }">
|
||||
<label class="control-label">Redirect URI</label>
|
||||
<div class="control-label" ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">
|
||||
<small ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">{{ clientAdd.error.redirectURI }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="clientAdd.redirectURI" name="redirectURI" id="clientAddRedirectURI" placeholder="Only required if OAuth logins are used">
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="clientAddForm.$invalid || clientAdd.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientAdd.busy"></i> Add OAuth Client</button>
|
||||
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientAdd.busy"></i> Add API Client</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Remove OAuth Client</h4>
|
||||
<h4 class="modal-title">Remove API Client</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientRemove.busy"></i> Remove OAuth Client</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientRemove.busy"></i> Remove API Client</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,11 +116,10 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>These tokens can be used to access the <a href="https://cloudron.io/references/api.html" target="_blank">Cloudron API</a>. They have the <b>admin</b> <a href="https://cloudron.io/references/api.html#scopes" target="_blank">scope</a> and do not expire.</p>
|
||||
<br/>
|
||||
<h4 class="text-muted">Active Tokens</h4>
|
||||
<hr/>
|
||||
<p ng-repeat="token in apiClient.activeTokens">
|
||||
<span ng-click-select>{{ token.accessToken }}</span> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
|
||||
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +130,7 @@
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>OAuth Apps<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New OAuth Client</button></h3>
|
||||
<h3>Apps<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New API Client</button></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +155,7 @@
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse{{client.id}}">Advanced</a>
|
||||
<div id="collapse{{client.id}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove OAuth Client" ng-show="client.type === 'external'">Remove OAuth Client</button></h4>
|
||||
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove API Client" ng-show="client.type === 'external'">Remove API Client</button></h4>
|
||||
<hr/>
|
||||
<p>Scope: <b ng-click-select>{{ client.scope }}</b></p>
|
||||
<p>RedirectURI: <b ng-click-select>{{ client.redirectURI }}</b></p>
|
||||
|
||||
@@ -19,7 +19,7 @@ angular.module('Application').controller('TokensController', ['$scope', 'Client'
|
||||
|
||||
$scope.clientAdd.error = {};
|
||||
$scope.clientAdd.name = '';
|
||||
$scope.clientAdd.scope = 'profile';
|
||||
$scope.clientAdd.scope = '*';
|
||||
$scope.clientAdd.redirectURI = '';
|
||||
|
||||
$scope.clientAddForm.$setUntouched();
|
||||
|
||||
@@ -221,8 +221,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>An email has been sent to <b>{{ inviteSent.email }}</b>.</p>
|
||||
<p>You can also share this invite link directly:</p>
|
||||
<p style="overflow: auto; white-space: nowrap;" eng-click-select>{{ inviteSent.setupLink }}</p>
|
||||
<p>You can also share this invite link directly.</p>
|
||||
<p ng-click-select>{{ inviteSent.setupLink }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
|
||||
|
||||
Reference in New Issue
Block a user