Compare commits

..

20 Commits

Author SHA1 Message Date
Johannes Zellner a50409bdca Also show errors above input fields for password reset 2017-03-20 16:50:31 +01:00
Johannes Zellner 60a722e6cc Remove superflous quote in html 2017-03-20 16:43:36 +01:00
Johannes Zellner 4d6cafa589 Show form errors on the top during user activation 2017-03-20 15:57:02 +01:00
Johannes Zellner 63e557430b ng-href takes a template string 2017-03-20 15:26:31 +01:00
Johannes Zellner 04acb4423d Add open registration rest api tests 2017-03-20 14:27:47 +01:00
Johannes Zellner ea813acf4c Add open registration default value and test 2017-03-20 14:27:39 +01:00
Johannes Zellner b1198dfdbf Add settingsdb tests for open registration 2017-03-20 14:22:11 +01:00
Johannes Zellner 4342de3747 Show error response on signup 2017-03-20 14:19:52 +01:00
Johannes Zellner ef8bc7e7e9 username must be null or non-empty string 2017-03-20 14:01:12 +01:00
Johannes Zellner e18e401f6b Improve signup form 2017-03-20 14:00:56 +01:00
Johannes Zellner ab998c47e8 Show user signup link if registration is open 2017-03-20 13:52:31 +01:00
Johannes Zellner 9fb830b2e1 add section to toggle open registration in settings view 2017-03-20 12:55:48 +01:00
Johannes Zellner 415c3f90a1 Always send an object with properties 2017-03-20 12:53:21 +01:00
Johannes Zellner 60c8ff7fb1 Add open_registration settings routes 2017-03-20 12:31:53 +01:00
Johannes Zellner 037816313c Remove newline 2017-03-20 12:29:15 +01:00
Johannes Zellner 3d285d1ac6 Better signup styling 2017-03-20 12:04:23 +01:00
Johannes Zellner 135338786f Protect user creation if open registration is not allowed 2017-03-20 12:00:58 +01:00
Johannes Zellner 661f1fce31 Angular uses double curly brackets 2017-03-20 11:58:01 +01:00
Johannes Zellner 03664ef784 Add open registration setting 2017-03-20 11:56:58 +01:00
Johannes Zellner d2111ef2b6 Add user signup ui 2017-03-20 11:52:11 +01:00
38 changed files with 375 additions and 108 deletions
-4
View File
@@ -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
-4
View File
@@ -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
-12
View File
@@ -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.
+6 -8
View File
@@ -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
View File
@@ -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 () {
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+1 -5
View File
@@ -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 {
<% } %>
}
}
+1 -1
View File
@@ -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' }
}
};
+1
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
listAliases: listAliases,
listMailboxes: listMailboxes,
// listGroups: listGroups, // this is beyond my SQL skillz
getMailbox: getMailbox,
getGroup: getGroup,
+48
View File
@@ -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 %>
+4 -4
View File
@@ -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"/>
-1
View File
@@ -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>
+2 -2
View File
@@ -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>
-1
View File
@@ -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} \
+84
View File
@@ -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,
+24
View File
@@ -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');
+41
View File
@@ -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
View File
@@ -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);
+30
View File
@@ -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');
+23
View File
@@ -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();
});
});
});
});
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
+14
View File
@@ -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
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
+2 -2
View File
@@ -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>
+10 -12
View File
@@ -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>&nbsp;<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>&nbsp;<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 ">
+1 -2
View File
@@ -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 : '';
+22
View File
@@ -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/>
+20
View File
@@ -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();
+15 -16
View File
@@ -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>
+1 -1
View File
@@ -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();
+2 -2
View File
@@ -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>