Add separate password reset view

This commit is contained in:
Johannes Zellner
2023-06-15 16:26:06 +02:00
parent b6739e9d77
commit 9d3fa94960
7 changed files with 340 additions and 5 deletions

View File

@@ -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();
}
}]);

View File

@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html>
<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 <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
<title>Cloudron Password Reset</title>
<meta name="description" content="Cloudron Password Reset">
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Theme CSS -->
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
<!-- Fontawesome -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
<!-- async -->
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
<!-- Bootstrap Core JavaScript -->
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
<!-- Showdown (markdown converter) -->
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
<!-- Angularjs scripts -->
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
<!-- <script type="text/javascript" src="/3rdparty/js/angular-md5.min.js"></script> -->
<!-- <script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script> -->
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/autofill-event.js?<%= revision %>"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></script>
<!-- Angular translate https://angular-translate.github.io/ -->
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
<!-- Setup Application -->
<script type="text/javascript" src="/js/passwordreset.js?<%= revision %>"></script>
</head>
<body ng-app="Application" ng-controller="PasswordResetController">
<div class="layout-root ng-cloak" ng-show="initialized">
<div class="layout-content" ng-show="mode === 'passwordReset'">
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
<br/>
<h2>{{ 'passwordReset.title' | tr }}</h2>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<form name="passwordResetForm" ng-submit="onPasswordReset()">
<div class="form-group">
<label class="control-label" for="inputPasswordResetIdentifier">{{ 'passwordReset.usernameOrEmail' | tr }}</label>
<input type="text" class="form-control" id="inputPasswordResetIdentifier" name="passwordResetIdentifier" ng-model="passwordResetIdentifier" ng-disabled="busy" autofocus required>
</div>
<br/>
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
</form>
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="mode === 'passwordResetDone'">
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
<br/>
<h2>{{ 'passwordReset.emailSent.title' | tr }}</h2>
<br/>
<a href="/" class="btn btn-primary">{{ 'passwordReset.backToLoginAction' | tr }}</a>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="mode === 'newPassword'">
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
<br/>
<h2>{{ 'passwordReset.newPassword.title' | tr }}</h2>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<h4 class="has-error" ng-show="error">{{ error }}</h4>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<form name="newPasswordForm" ng-submit="onNewPassword()">
<input type="password" style="display: none;"/>
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid }">
<label class="control-label" for="inputNewPassword">{{ 'passwordReset.newPassword.password' | tr }}</label>
<div class="control-label" ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">
<small ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">{{ 'passwordReset.newPassword.errorLength' | tr }}</small>
</div>
<input type="password" class="form-control" id="inputNewPassword" ng-model="newPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" autofocus required password-reveal>
</div>
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat) }">
<label class="control-label" for="inputNewPasswordRepeat">{{ 'passwordReset.newPassword.passwordRepeat' | tr }}</label>
<div class="control-label" ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">
<small ng-show="newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat)">{{ 'passwordReset.newPassword.errorMismatch' | tr }}</small>
</div>
<input type="password" class="form-control" id="inputNewPasswordRepeat" ng-model="newPasswordRepeat" name="newPasswordRepeat" required password-reveal>
</div>
<div class="form-group">
<label class="control-label" for="inputPasswordResetTotpToken">{{ 'login.2faToken' | tr }}</label>
<input type="text" class="form-control" name="passwordResetTotpToken" id="inputPasswordResetTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
</div>
<br/>
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
</form>
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="mode === 'newPasswordDone'">
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
<br/>
<h2>{{ 'passwordReset.success.title' | tr }}</h2>
<br/>
<a href="/" class="btn btn-primary">{{ 'passwordReset.success.openDashboardAction' | tr }}</a>
</div>
</div>
</div>
</div>
<footer class="text-center">
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
</footer>
</div>
</body>
</html>