diff --git a/dashboard/gulpfile.js b/dashboard/gulpfile.js index 596d0705f..8e55f5520 100644 --- a/dashboard/gulpfile.js +++ b/dashboard/gulpfile.js @@ -151,6 +151,15 @@ gulp.task('js-login', function () { .pipe(gulp.dest('dist/js')); }); +gulp.task('js-passwordreset', function () { + return gulp.src(['src/js/passwordreset.js', 'src/js/utils.js']) + .pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' })) + .pipe(sourcemaps.init()) + .pipe(concat('passwordreset.js', { newLine: ';' })) + .pipe(sourcemaps.write()) + .pipe(gulp.dest('dist/js')); +}); + gulp.task('js-setupaccount', function () { return gulp.src(['src/js/setupaccount.js', 'src/js/utils.js']) .pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' })) @@ -187,7 +196,7 @@ gulp.task('js-restore', function () { .pipe(gulp.dest('dist/js')); }); -gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-filemanager', 'js-terminal', 'js-login', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ])); +gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-filemanager', 'js-terminal', 'js-login', 'js-passwordreset', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ])); // -------------- // HTML @@ -266,6 +275,7 @@ gulp.task('watch', function (done) { gulp.watch(['src/js/filemanager.js', 'src/js/client.js', 'src/js/utils.js', 'src/components/*.js'], gulp.series(['js-filemanager'])); gulp.watch(['src/js/terminal.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-terminal'])); gulp.watch(['src/js/login.js', 'src/js/utils.js'], gulp.series(['js-login'])); + gulp.watch(['src/js/passwordreset.js', 'src/js/utils.js'], gulp.series(['js-passwordreset'])); gulp.watch(['src/js/setupaccount.js', 'src/js/utils.js'], gulp.series(['js-setupaccount'])); gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/main.js', 'src/views/*.js', 'src/js/utils.js'], gulp.series(['js-index'])); gulp.watch(['src/3rdparty/**/*'], gulp.series(['3rdparty'])); diff --git a/dashboard/src/js/passwordreset.js b/dashboard/src/js/passwordreset.js new file mode 100644 index 000000000..b436b70a8 --- /dev/null +++ b/dashboard/src/js/passwordreset.js @@ -0,0 +1,158 @@ +'use strict'; + +/* global angular, $, showdown */ + +// create main application module +var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies']); + +app.filter('markdown2html', function () { + var converter = new showdown.Converter({ + simplifiedAutoLink: true, + strikethrough: true, + tables: true, + openLinksInNewWindow: true + }); + + return function (text) { + return converter.makeHtml(text); + }; +}); + +// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce +app.config(function ($sceProvider) { + $sceProvider.enabled(false); +}); + +app.config(['$translateProvider', function ($translateProvider) { + $translateProvider.useStaticFilesLoader({ + prefix: 'translation/', + suffix: '.json' + }); + $translateProvider.preferredLanguage('en'); + $translateProvider.fallbackLanguage('en'); +}]); + +// Add shorthand "tr" filter to avoid having ot use "translate" +// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js +// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required +function translateFilterFactory($parse, $translate) { + var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) { + if (!angular.isObject(interpolateParams)) { + var ctx = this || { + '__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f' + }; + interpolateParams = $parse(interpolateParams)(ctx); + } + + return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage); + }; + + if ($translate.statefulFilter()) { + translateFilter.$stateful = true; + } + + return translateFilter; +} +translateFilterFactory.displayName = 'translateFilterFactory'; +app.filter('tr', translateFilterFactory); + + +app.controller('PasswordResetController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) { + // Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own + var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); + + $scope.initialized = false; + $scope.mode = ''; + $scope.busy = false; + $scope.error = false; + $scope.status = null; + $scope.username = ''; + $scope.password = ''; + $scope.totpToken = ''; + $scope.passwordResetIdentifier = ''; + $scope.newPassword = ''; + $scope.newPasswordRepeat = ''; + var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin; + + $scope.onPasswordReset = function () { + $scope.busy = true; + + var data = { + identifier: $scope.passwordResetIdentifier + }; + + function done() { + $scope.busy = false; + $scope.mode = 'passwordResetDone'; + } + + $http.post(API_ORIGIN + '/api/v1/cloudron/password_reset_request', data).success(done).error(done); + }; + + $scope.onNewPassword = function () { + $scope.busy = true; + + var data = { + resetToken: search.resetToken, + password: $scope.newPassword, + totpToken: $scope.totpToken + }; + + function error(data, status) { + console.log('error', status); + $scope.busy = false; + + if (status === 401) $scope.error = data.message; + else if (status === 409) $scope.error = 'Ask your admin for an invite link first'; + else $scope.error = 'Unknown error'; + } + + $http.post(API_ORIGIN + '/api/v1/cloudron/password_reset', data).success(function (data, status) { + if (status !== 202) return error(data, status); + + // set token to autologin + localStorage.token = data.accessToken; + + $scope.mode = 'newPasswordDone'; + }).error(function (data, status) { + error(data, status); + }); + }; + + $scope.showPasswordReset = function () { + window.document.title = 'Password Reset Request'; + $scope.mode = 'passwordReset'; + $scope.passwordResetIdentifier = ''; + setTimeout(function () { $('#inputPasswordResetIdentifier').focus(); }, 200); + }; + + $scope.showNewPassword = function () { + window.document.title = 'Set New Password'; + $scope.mode = 'newPassword'; + setTimeout(function () { $('#inputNewPassword').focus(); }, 200); + }; + + $http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) { + $scope.initialized = true; + + if (status !== 200) return; + + if (data.language) $translate.use(data.language); + + $scope.status = data; + }).error(function () { + $scope.initialized = false; + }); + + // Init into the correct view + if (search.passwordReset) { + $scope.showPasswordReset(); + } else if (search.resetToken) { + $scope.showNewPassword(); + } else if (search.accessToken || search.access_token) { // auto-login feature + localStorage.token = search.accessToken || search.access_token; + window.location.href = '/'; + } else { + $scope.showPasswordReset(); + } +}]); diff --git a/dashboard/src/passwordreset.html b/dashboard/src/passwordreset.html new file mode 100644 index 000000000..ee703b6d6 --- /dev/null +++ b/dashboard/src/passwordreset.html @@ -0,0 +1,167 @@ + + +
+ + + + + +