mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-23 09:08:26 +01:00
updates for forgot-password and reset-password of UI
This commit is contained in:
parent
475ef0a079
commit
8d7bb89e4f
@ -1,20 +1,111 @@
|
||||
package ng
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/beego/i18n"
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
type BaseController struct {
|
||||
beego.Controller
|
||||
i18n.Locale
|
||||
SelfRegistration bool
|
||||
IsAdmin bool
|
||||
AuthMode string
|
||||
}
|
||||
|
||||
type langType struct {
|
||||
Lang string
|
||||
Name string
|
||||
}
|
||||
|
||||
const (
|
||||
viewPath = "sections"
|
||||
prefixNg = "ng"
|
||||
viewPath = "sections"
|
||||
prefixNg = "ng"
|
||||
defaultLang = "en-US"
|
||||
)
|
||||
|
||||
var supportLanguages map[string]langType
|
||||
|
||||
// Prepare extracts the language information from request and populate data for rendering templates.
|
||||
func (b *BaseController) Prepare() {
|
||||
|
||||
var lang string
|
||||
al := b.Ctx.Request.Header.Get("Accept-Language")
|
||||
|
||||
if len(al) > 4 {
|
||||
al = al[:5] // Only compare first 5 letters.
|
||||
if i18n.IsExist(al) {
|
||||
lang = al
|
||||
}
|
||||
}
|
||||
|
||||
if _, exist := supportLanguages[lang]; exist == false { //Check if support the request language.
|
||||
lang = defaultLang //Set default language if not supported.
|
||||
}
|
||||
|
||||
sessionLang := b.GetSession("lang")
|
||||
if sessionLang != nil {
|
||||
b.SetSession("Lang", lang)
|
||||
lang = sessionLang.(string)
|
||||
}
|
||||
|
||||
curLang := langType{
|
||||
Lang: lang,
|
||||
}
|
||||
|
||||
restLangs := make([]*langType, 0, len(langTypes)-1)
|
||||
for _, v := range langTypes {
|
||||
if lang != v.Lang {
|
||||
restLangs = append(restLangs, v)
|
||||
} else {
|
||||
curLang.Name = v.Name
|
||||
}
|
||||
}
|
||||
|
||||
// Set language properties.
|
||||
b.Lang = lang
|
||||
b.Data["Lang"] = curLang.Lang
|
||||
b.Data["CurLang"] = curLang.Name
|
||||
b.Data["RestLangs"] = restLangs
|
||||
|
||||
authMode := strings.ToLower(os.Getenv("AUTH_MODE"))
|
||||
if authMode == "" {
|
||||
authMode = "db_auth"
|
||||
}
|
||||
b.AuthMode = authMode
|
||||
b.Data["AuthMode"] = b.AuthMode
|
||||
|
||||
selfRegistration := strings.ToLower(os.Getenv("SELF_REGISTRATION"))
|
||||
|
||||
if selfRegistration == "on" {
|
||||
b.SelfRegistration = true
|
||||
}
|
||||
|
||||
sessionUserID := b.GetSession("userId")
|
||||
if sessionUserID != nil {
|
||||
b.Data["Username"] = b.GetSession("username")
|
||||
b.Data["UserId"] = sessionUserID.(int)
|
||||
|
||||
var err error
|
||||
b.IsAdmin, err = dao.IsAdminRole(sessionUserID.(int))
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in IsAdminRole:%v", err)
|
||||
b.CustomAbort(http.StatusInternalServerError, "Internal error.")
|
||||
}
|
||||
}
|
||||
|
||||
b.Data["IsAdmin"] = b.IsAdmin
|
||||
b.Data["SelfRegistration"] = b.SelfRegistration
|
||||
|
||||
}
|
||||
|
||||
func (bc *BaseController) Forward(title, templateName string) {
|
||||
bc.Layout = filepath.Join(prefixNg, "layout.htm")
|
||||
bc.TplName = filepath.Join(prefixNg, templateName)
|
||||
@ -26,3 +117,41 @@ func (bc *BaseController) Forward(title, templateName string) {
|
||||
bc.LayoutSections["FooterContent"] = filepath.Join(prefixNg, viewPath, "footer-content.htm")
|
||||
|
||||
}
|
||||
|
||||
var langTypes []*langType
|
||||
|
||||
func init() {
|
||||
|
||||
//conf/app.conf -> os.Getenv("config_path")
|
||||
configPath := os.Getenv("CONFIG_PATH")
|
||||
if len(configPath) != 0 {
|
||||
log.Infof("Config path: %s", configPath)
|
||||
beego.AppConfigPath = configPath
|
||||
if err := beego.ParseConfig(); err != nil {
|
||||
log.Warningf("Failed to parse config file: %s, error: %v", configPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
beego.AddFuncMap("i18n", i18n.Tr)
|
||||
|
||||
langs := strings.Split(beego.AppConfig.String("lang::types"), "|")
|
||||
names := strings.Split(beego.AppConfig.String("lang::names"), "|")
|
||||
|
||||
supportLanguages = make(map[string]langType)
|
||||
|
||||
langTypes = make([]*langType, 0, len(langs))
|
||||
for i, v := range langs {
|
||||
t := langType{
|
||||
Lang: v,
|
||||
Name: names[i],
|
||||
}
|
||||
langTypes = append(langTypes, &t)
|
||||
supportLanguages[v] = t
|
||||
}
|
||||
|
||||
for _, lang := range langs {
|
||||
if err := i18n.SetMessage(lang, "static/i18n/"+"locale_"+lang+".ini"); err != nil {
|
||||
log.Errorf("Fail to set message file: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
175
controllers/ng/password.go
Normal file
175
controllers/ng/password.go
Normal file
@ -0,0 +1,175 @@
|
||||
package ng
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"text/template"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/vmware/harbor/dao"
|
||||
"github.com/vmware/harbor/models"
|
||||
"github.com/vmware/harbor/utils"
|
||||
"github.com/vmware/harbor/utils/log"
|
||||
)
|
||||
|
||||
type CommonController struct {
|
||||
BaseController
|
||||
}
|
||||
|
||||
func (cc *CommonController) Render() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type messageDetail struct {
|
||||
Hint string
|
||||
URL string
|
||||
UUID string
|
||||
}
|
||||
|
||||
// SendEmail verifies the Email address and contact SMTP server to send reset password Email.
|
||||
func (cc *CommonController) SendEmail() {
|
||||
|
||||
email := cc.GetString("email")
|
||||
|
||||
pass, _ := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, email)
|
||||
|
||||
if !pass {
|
||||
cc.CustomAbort(http.StatusBadRequest, "email_content_illegal")
|
||||
} else {
|
||||
|
||||
queryUser := models.User{Email: email}
|
||||
exist, err := dao.UserExists(queryUser, "email")
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in UserExists: %v", err)
|
||||
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
|
||||
}
|
||||
if !exist {
|
||||
cc.CustomAbort(http.StatusNotFound, "email_does_not_exist")
|
||||
}
|
||||
|
||||
messageTemplate, err := template.ParseFiles("views/ng/reset-password-mail.tpl")
|
||||
if err != nil {
|
||||
log.Errorf("Parse email template file failed: %v", err)
|
||||
cc.CustomAbort(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
message := new(bytes.Buffer)
|
||||
|
||||
harborURL := os.Getenv("HARBOR_URL")
|
||||
if harborURL == "" {
|
||||
harborURL = "localhost"
|
||||
}
|
||||
uuid, err := dao.GenerateRandomString()
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in GenerateRandomString: %v", err)
|
||||
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
|
||||
}
|
||||
err = messageTemplate.Execute(message, messageDetail{
|
||||
Hint: cc.Tr("reset_email_hint"),
|
||||
URL: harborURL,
|
||||
UUID: uuid,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Message template error: %v", err)
|
||||
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
|
||||
}
|
||||
|
||||
config, err := beego.AppConfig.GetSection("mail")
|
||||
if err != nil {
|
||||
log.Errorf("Can not load app.conf: %v", err)
|
||||
cc.CustomAbort(http.StatusInternalServerError, "internal_error")
|
||||
}
|
||||
|
||||
mail := utils.Mail{
|
||||
From: config["from"],
|
||||
To: []string{email},
|
||||
Subject: cc.Tr("reset_email_subject"),
|
||||
Message: message.String()}
|
||||
|
||||
err = mail.SendMail()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Send email failed: %v", err)
|
||||
cc.CustomAbort(http.StatusInternalServerError, "send_email_failed")
|
||||
}
|
||||
|
||||
user := models.User{ResetUUID: uuid, Email: email}
|
||||
dao.UpdateUserResetUUID(user)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type ForgotPasswordController struct {
|
||||
BaseController
|
||||
}
|
||||
|
||||
func (fpc *ForgotPasswordController) Get() {
|
||||
fpc.Forward("Forgot Password", "forgot-password.htm")
|
||||
}
|
||||
|
||||
// ResetPasswordController handles request to /resetPassword
|
||||
type ResetPasswordController struct {
|
||||
BaseController
|
||||
}
|
||||
|
||||
// Get checks if reset_uuid in the reset link is valid and render the result page for user to reset password.
|
||||
func (rpc *ResetPasswordController) Get() {
|
||||
|
||||
resetUUID := rpc.GetString("reset_uuid")
|
||||
if resetUUID == "" {
|
||||
log.Error("Reset uuid is blank.")
|
||||
rpc.Redirect("/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
queryUser := models.User{ResetUUID: resetUUID}
|
||||
user, err := dao.GetUser(queryUser)
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in GetUser: %v", err)
|
||||
rpc.CustomAbort(http.StatusInternalServerError, "Internal error.")
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
rpc.Data["ResetUuid"] = user.ResetUUID
|
||||
rpc.Forward("Reset Password", "reset-password.htm")
|
||||
} else {
|
||||
rpc.Redirect("/", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// ResetPassword handles request from the reset page and reset password
|
||||
func (cc *CommonController) ResetPassword() {
|
||||
|
||||
resetUUID := cc.GetString("reset_uuid")
|
||||
if resetUUID == "" {
|
||||
cc.CustomAbort(http.StatusBadRequest, "Reset uuid is blank.")
|
||||
}
|
||||
|
||||
queryUser := models.User{ResetUUID: resetUUID}
|
||||
user, err := dao.GetUser(queryUser)
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in GetUser: %v", err)
|
||||
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
|
||||
}
|
||||
if user == nil {
|
||||
log.Error("User does not exist")
|
||||
cc.CustomAbort(http.StatusBadRequest, "User does not exist")
|
||||
}
|
||||
|
||||
password := cc.GetString("password")
|
||||
|
||||
if password != "" {
|
||||
user.Password = password
|
||||
err = dao.ResetUserPassword(*user)
|
||||
if err != nil {
|
||||
log.Errorf("Error occurred in ResetUserPassword: %v", err)
|
||||
cc.CustomAbort(http.StatusInternalServerError, "Internal error.")
|
||||
}
|
||||
} else {
|
||||
cc.CustomAbort(http.StatusBadRequest, "password_is_required")
|
||||
}
|
||||
}
|
@ -14,4 +14,6 @@ func initNgRouters() {
|
||||
beego.Router("/ng/repository", &ng.RepositoryController{})
|
||||
beego.Router("/ng/sign_up", &ng.SignUpController{})
|
||||
beego.Router("/ng/account_setting", &ng.AccountSettingController{})
|
||||
beego.Router("/ng/forgot_password", &ng.ForgotPasswordController{})
|
||||
beego.Router("/ng/reset_password", &ng.ResetPasswordController{})
|
||||
}
|
||||
|
@ -56,3 +56,67 @@ nav .container-custom {
|
||||
.nav-custom .active {
|
||||
border-bottom: 3px solid #EFEFEF;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.dropdown .btn-link:hover,
|
||||
.dropdown .btn-link:visited,
|
||||
.dropdown .btn-link:link {
|
||||
display:inline-block;
|
||||
text-decoration: none;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
left: 9%;
|
||||
}
|
||||
|
||||
.dropdown-submenu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-submenu>.dropdown-menu {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
margin-top: -6px;
|
||||
margin-left: -1px;
|
||||
-webkit-border-radius: 0 6px 6px 6px;
|
||||
-moz-border-radius: 0 6px 6px;
|
||||
border-radius: 0 6px 6px 6px;
|
||||
}
|
||||
|
||||
.dropdown-submenu:hover>.dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-submenu>a:after {
|
||||
display: block;
|
||||
content: " ";
|
||||
float: right;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-left-color: #ccc;
|
||||
margin-top: 5px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
.dropdown-submenu:hover>a:after {
|
||||
border-left-color: #fff;
|
||||
}
|
||||
|
||||
.dropdown-submenu.pull-left {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.dropdown-submenu.pull-left>.dropdown-menu {
|
||||
left: -100%;
|
||||
margin-left: 10px;
|
||||
-webkit-border-radius: 6px 0 6px 6px;
|
||||
-moz-border-radius: 6px 0 6px 6px;
|
||||
border-radius: 6px 0 6px 6px;
|
||||
}
|
@ -27,6 +27,7 @@ body {
|
||||
.thumbnail {
|
||||
display: inline-block;
|
||||
border: none;
|
||||
padding: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
@ -92,6 +92,7 @@
|
||||
restrict: 'E',
|
||||
templateUrl: '/static/ng/resources/js/components/log/list-log.directive.html',
|
||||
replace: true,
|
||||
scope: true,
|
||||
controller: ListLogController,
|
||||
controllerAs: 'vm',
|
||||
bindToController: true
|
||||
|
@ -1,22 +1,30 @@
|
||||
<ul class="nav navbar-nav navbar-left">
|
||||
<li ng-show="//!vm.isLoggedIn//" class="dropdown">
|
||||
<ul ng-if="!vm.isLoggedIn" class="nav navbar-nav navbar-left">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-globe"></span>Language<span class="caret"></span>
|
||||
<span class="glyphicon glyphicon-globe"></span> Language<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#">//vm.isLoggedIn ? 'Logged In' : 'Not Logged In'//</a></li>
|
||||
<ul class="dropdown-menu" style="left: 0;">
|
||||
<li><a href="#">English</a></li>
|
||||
<li><a href="#">中文</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li ng-show="//vm.isLoggedIn//" class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-user"></span>//vm.username//<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#">//vm.username//</a></li>
|
||||
<li><a href="/ng/account_setting">Account Setting</a></li>
|
||||
<li><a href="#">Log Out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<div ng-if="vm.isLoggedIn" class="dropdown">
|
||||
<a id="dLabel" role="button" data-toggle="dropdown" class="btn btn-link" data-target="#" href="">
|
||||
<span class="glyphicon glyphicon-user"></span> //vm.username//
|
||||
</a>
|
||||
<ul class="dropdown-menu multi-level" role="menu" aria-labelledby="dropdownMenu">
|
||||
<li><a href="/ng/account_setting"><span class="glyphicon glyphicon-pencil"></span> Account Setting</a></li>
|
||||
<li class="dropdown-submenu">
|
||||
<a tabindex="-1" href="#"><span class="glyphicon glyphicon-globe"></span> Languages</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#">English</a></li>
|
||||
<li><a href="#">中文</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#"><span class="glyphicon glyphicon-log-out"></span> Log Out</a></li>
|
||||
</ul>
|
||||
</div>
|
@ -8,41 +8,30 @@
|
||||
|
||||
OptionalMenuController.$inject = ['$scope'];
|
||||
|
||||
function OptionalMenuController($scope, CurrentUserService) {
|
||||
function OptionalMenuController($scope, $timeout) {
|
||||
var vm = this;
|
||||
vm.username = 'abcde';
|
||||
$scope.$watch('vm.username', function(current) {
|
||||
if(current) {
|
||||
vm.username = current;
|
||||
console.log('vm.username:' + current);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function optionalMenu() {
|
||||
var directive = {
|
||||
'restrict': 'E',
|
||||
'templateUrl': '/static/ng/resources/js/components/optional-menu/optional-menu.directive.html',
|
||||
'scope': {
|
||||
'isLoggedIn': '=',
|
||||
'username': '='
|
||||
},
|
||||
'link': link,
|
||||
'scope': true,
|
||||
'controller': OptionalMenuController,
|
||||
'controllerAs': 'vm',
|
||||
'bindToController': true
|
||||
};
|
||||
return directive;
|
||||
|
||||
function link(scope, element, attrs, ctrl) {
|
||||
scope.$watch('vm.isLoggedIn', function(current) {
|
||||
if(current) {
|
||||
ctrl.isLoggedIn = current;
|
||||
console.log('vm.isLoggedIn:' + current);
|
||||
ctrl.isLoggedIn = false;
|
||||
scope.$on('currentUser', function(e, val) {
|
||||
if(val != null) {
|
||||
ctrl.isLoggedIn = true;
|
||||
ctrl.username = val.username;
|
||||
}
|
||||
scope.$apply();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,9 @@
|
||||
.module('harbor.project.member')
|
||||
.directive('addProjectMember', addProjectMember);
|
||||
|
||||
AddProjectMemberController.$inject = ['roles', 'AddProjectMemberService'];
|
||||
AddProjectMemberController.$inject = ['$scope', 'roles', 'AddProjectMemberService'];
|
||||
|
||||
function AddProjectMemberController(roles, AddProjectMemberService) {
|
||||
function AddProjectMemberController($scope, roles, AddProjectMemberService) {
|
||||
var vm = this;
|
||||
vm.username = "";
|
||||
vm.roles = roles();
|
||||
@ -17,7 +17,7 @@
|
||||
vm.cancel = cancel;
|
||||
|
||||
function save() {
|
||||
AddProjectMemberService(2, vm.optRole, vm.username)
|
||||
AddProjectMemberService(vm.projectId, vm.optRole, vm.username)
|
||||
.success(addProjectMemberComplete)
|
||||
.error(addProjectMemberFailed);
|
||||
vm.username = "";
|
||||
@ -46,6 +46,7 @@
|
||||
'restrict': 'E',
|
||||
'templateUrl': '/static/ng/resources/js/components/project-member/add-project-member.directive.html',
|
||||
'scope': {
|
||||
'projectId': '@',
|
||||
'isOpen': '=',
|
||||
'reload': '&'
|
||||
},
|
||||
|
@ -11,7 +11,7 @@
|
||||
<button ng-if="vm.isOpen" class="btn btn-default" disabled="disabled" type="button"><span class="glyphicon glyphicon-plus"></span>Add Member</button>
|
||||
</div>
|
||||
<div class="pane">
|
||||
<add-project-member ng-show="vm.isOpen" is-open="vm.isOpen" reload='vm.search({projectId: vm.projectId, username: vm.username})'></add-project-member>
|
||||
<add-project-member ng-show="vm.isOpen" is-open="vm.isOpen" project-id="//vm.projectId//" reload='vm.search({projectId: vm.projectId, username: vm.username})'></add-project-member>
|
||||
<div class="sub-pane">
|
||||
<table class="table table-pane" >
|
||||
<thead>
|
||||
|
@ -6,10 +6,17 @@
|
||||
.module('harbor.project.member')
|
||||
.directive('listProjectMember', listProjectMember);
|
||||
|
||||
ListProjectMemberController.$inject = ['$scope', 'CurrentUserService', 'ListProjectMemberService', '$routeParams'];
|
||||
ListProjectMemberController.$inject = ['$scope', 'ListProjectMemberService', '$routeParams'];
|
||||
|
||||
function ListProjectMemberController($scope, CurrentUserService, ListProjectMemberService, $routeParams) {
|
||||
function ListProjectMemberController($scope, ListProjectMemberService, $routeParams) {
|
||||
var vm = this;
|
||||
vm.currentUser = {};
|
||||
|
||||
$scope.$on('currentUser', function(e, val) {
|
||||
vm.currentUser = val;
|
||||
console.log('In list-project-member received current user:' + vm.currentUser);
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
vm.isOpen = false;
|
||||
|
||||
@ -19,6 +26,7 @@
|
||||
|
||||
vm.projectId = $routeParams.project_id;
|
||||
vm.username = "";
|
||||
vm.currentUser = {};
|
||||
|
||||
vm.retrieve();
|
||||
|
||||
@ -38,32 +46,19 @@
|
||||
}
|
||||
|
||||
function retrieve() {
|
||||
$.when(
|
||||
CurrentUserService()
|
||||
.success(getCurrentUserSuccess)
|
||||
.error(getCurrentUserFailed))
|
||||
.then(function(){
|
||||
ListProjectMemberService(vm.projectId, {'username': vm.username})
|
||||
.then(getProjectMemberComplete)
|
||||
.catch(getProjectMemberFailed);
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentUserSuccess(data, status) {
|
||||
vm.currentUser = data;
|
||||
}
|
||||
ListProjectMemberService(vm.projectId, {'username': vm.username})
|
||||
.then(getProjectMemberComplete)
|
||||
.catch(getProjectMemberFailed);
|
||||
|
||||
function getCurrentUserFailed(e) {
|
||||
console.log('Failed in getCurrentUser:' + e);
|
||||
}
|
||||
|
||||
|
||||
function getProjectMemberComplete(response) {
|
||||
vm.projectMembers = response.data;
|
||||
}
|
||||
|
||||
function getProjectMemberFailed(response) {
|
||||
|
||||
console.log('Failed get project members:' + response);
|
||||
}
|
||||
|
||||
}
|
||||
@ -73,6 +68,7 @@
|
||||
restrict: 'E',
|
||||
templateUrl: '/static/ng/resources/js/components/project-member/list-project-member.directive.html',
|
||||
replace: true,
|
||||
scope: true,
|
||||
link: link,
|
||||
controller: ListProjectMemberController,
|
||||
controllerAs: 'vm',
|
||||
|
@ -38,6 +38,7 @@
|
||||
var directive = {
|
||||
'restrict': 'E',
|
||||
'templateUrl': '/static/ng/resources/js/components/sign-in/sign-in.directive.html',
|
||||
'scope': true,
|
||||
'controller': SignInController,
|
||||
'controllerAs': 'vm',
|
||||
'bindToController': true
|
||||
|
@ -25,9 +25,6 @@
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -4,10 +4,13 @@
|
||||
.module('harbor.app', [
|
||||
'ngRoute',
|
||||
'ngMessages',
|
||||
'harbor.session',
|
||||
'harbor.layout.header',
|
||||
'harbor.layout.navigation',
|
||||
'harbor.layout.sign.up',
|
||||
'harbor.layout.account.setting',
|
||||
'harbor.layout.forgot.password',
|
||||
'harbor.layout.reset.password',
|
||||
'harbor.layout.index',
|
||||
'harbor.layout.project',
|
||||
'harbor.layout.repository',
|
||||
@ -18,7 +21,6 @@
|
||||
'harbor.services.user',
|
||||
'harbor.services.repository',
|
||||
'harbor.services.project.member',
|
||||
'harbor.session',
|
||||
'harbor.optional.menu',
|
||||
'harbor.sign.in',
|
||||
'harbor.search',
|
||||
|
@ -6,9 +6,9 @@
|
||||
.module('harbor.layout.account.setting')
|
||||
.controller('AccountSettingController', AccountSettingController);
|
||||
|
||||
AccountSettingController.$inject = ['CurrentUserService', 'ChangePasswordService', '$window'];
|
||||
AccountSettingController.$inject = ['ChangePasswordService', '$scope', '$window'];
|
||||
|
||||
function AccountSettingController(CurrentUserService, ChangePasswordService, $window) {
|
||||
function AccountSettingController(ChangePasswordService, $scope, $window) {
|
||||
var vm = this;
|
||||
vm.isOpen = false;
|
||||
vm.user = {};
|
||||
@ -18,9 +18,9 @@
|
||||
vm.changePassword= changePassword;
|
||||
vm.cancel = cancel;
|
||||
|
||||
CurrentUserService()
|
||||
.success(getCurrentUserSuccess)
|
||||
.error(getCurrentUserFailed);
|
||||
$scope.$on('currentUser', function(e, val) {
|
||||
vm.user = val;
|
||||
});
|
||||
|
||||
function toggleChangePassword() {
|
||||
if(vm.isOpen) {
|
||||
@ -31,11 +31,6 @@
|
||||
console.log('vm.isOpen:' + vm.isOpen);
|
||||
}
|
||||
|
||||
function getCurrentUserSuccess(data, status) {
|
||||
vm.user = angular.copy(data);
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
function getCurrentUserFailed(data) {
|
||||
console.log('Failed get current user:' + data);
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.forgot.password')
|
||||
.controller('ForgotPasswordController', ForgotPasswordController);
|
||||
|
||||
ForgotPasswordController.$inject = ['SendMailService'];
|
||||
|
||||
function ForgotPasswordController(SendMailService) {
|
||||
var vm = this;
|
||||
vm.hasError = false;
|
||||
vm.errorMessage = '';
|
||||
vm.sendMail = sendMail;
|
||||
|
||||
function sendMail(user) {
|
||||
vm.hasError = false;
|
||||
console.log('Email address:' + user.email);
|
||||
SendMailService(user.email)
|
||||
.success(sendMailSuccess)
|
||||
.error(sendMailFailed);
|
||||
}
|
||||
|
||||
function sendMailSuccess(data, status) {
|
||||
console.log('Successful send mail:' + data);
|
||||
}
|
||||
|
||||
function sendMailFailed(data) {
|
||||
vm.hasError = true;
|
||||
vm.errorMessage = data;
|
||||
console.log('Failed send mail:' + data);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
@ -0,0 +1,10 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.forgot.password', [
|
||||
'harbor.services.user'
|
||||
]);
|
||||
|
||||
})();
|
@ -6,27 +6,10 @@
|
||||
.module('harbor.layout.header')
|
||||
.controller('HeaderController', HeaderController);
|
||||
|
||||
HeaderController.$inject = ['CurrentUserService', '$scope'];
|
||||
HeaderController.$inject = ['$scope'];
|
||||
|
||||
function HeaderController(CurrentUserService, $scope) {
|
||||
function HeaderController($scope) {
|
||||
var vm = this;
|
||||
|
||||
vm.isLoggedIn = true;
|
||||
vm.currentUser = {};
|
||||
|
||||
CurrentUserService()
|
||||
.then(currentUserSucess)
|
||||
.catch(currentUserFailed);
|
||||
|
||||
function currentUserSucess(response) {
|
||||
vm.isLoggedIn = true;
|
||||
vm.currentUser.username = response.data.username;
|
||||
console.log('vm.currentUser.username:' + vm.currentUser.username);
|
||||
}
|
||||
|
||||
function currentUserFailed(e) {
|
||||
// vm.isLoggedIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
@ -18,7 +18,7 @@
|
||||
restrict: 'E',
|
||||
templateUrl: '/static/ng/resources/js/layout/navigation/navigation-header.directive.html',
|
||||
link: link,
|
||||
replace: true,
|
||||
scope: true,
|
||||
controller: NavigationHeaderController,
|
||||
controllerAs: 'vm',
|
||||
bindToController: true
|
||||
|
@ -24,15 +24,14 @@
|
||||
vm.retrieve();
|
||||
|
||||
function retrieve() {
|
||||
$.when(
|
||||
CurrentUserService()
|
||||
.success(getCurrentUserSuccess)
|
||||
.error(getCurrentUserFailed))
|
||||
.then(function(){
|
||||
ListProjectService(vm.projectName, vm.publicity)
|
||||
.success(listProjectSuccess)
|
||||
.error(listProjectFailed);
|
||||
|
||||
$scope.$on('currentUser', function(e, val) {
|
||||
vm.currentUser = val;
|
||||
});
|
||||
|
||||
ListProjectService(vm.projectName, vm.publicity)
|
||||
.success(listProjectSuccess)
|
||||
.error(listProjectFailed);
|
||||
}
|
||||
|
||||
function listProjectSuccess(data, status) {
|
||||
@ -43,14 +42,6 @@
|
||||
console.log('Failed to list Project:' + e);
|
||||
}
|
||||
|
||||
function getCurrentUserSuccess(data, status) {
|
||||
vm.currentUser = data;
|
||||
}
|
||||
|
||||
function getCurrentUserFailed(e) {
|
||||
console.log('Failed in getCurrentUser:' + e);
|
||||
}
|
||||
|
||||
$scope.$on('addedSuccess', function(e, val) {
|
||||
vm.retrieve();
|
||||
});
|
||||
|
@ -0,0 +1,44 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.reset.password')
|
||||
.controller('ResetPasswordController', ResetPasswordController);
|
||||
|
||||
ResetPasswordController.$inject = ['$location', 'ResetPasswordService'];
|
||||
|
||||
function getParameterByName(name, url) {
|
||||
name = name.replace(/[\[\]]/g, "\\$&");
|
||||
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
function ResetPasswordController($location, ResetPasswordService) {
|
||||
var vm = this;
|
||||
vm.resetUuid = getParameterByName('reset_uuid', $location.absUrl());
|
||||
vm.resetPassword = resetPassword;
|
||||
console.log(vm.resetUuid);
|
||||
function resetPassword(user) {
|
||||
console.log('rececived password:' + user.password + ', reset_uuid:' + vm.resetUuid);
|
||||
ResetPasswordService(vm.resetUuid, user.password)
|
||||
.success(resetPasswordSuccess)
|
||||
.error(resetPasswordFailed);
|
||||
}
|
||||
|
||||
function resetPasswordSuccess(data, status) {
|
||||
console.log('Successful reset password:' + data);
|
||||
}
|
||||
|
||||
function resetPasswordFailed(data) {
|
||||
console.log('Failed reset password:' + data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -0,0 +1,10 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.layout.reset.password', [
|
||||
'harbor.services.user'
|
||||
]);
|
||||
|
||||
})();
|
@ -1,21 +0,0 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.services.user')
|
||||
.factory('ForgotPasswordService', ForgotPasswordService);
|
||||
|
||||
ForgotPasswordService.$inject = ['$http', '$log'];
|
||||
|
||||
function ForgotPasswordService($http, $log) {
|
||||
|
||||
return ForgotPassword;
|
||||
|
||||
function ForgotPassword(user) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -0,0 +1,29 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.services.user')
|
||||
.factory('ResetPasswordService', ResetPasswordService);
|
||||
|
||||
ResetPasswordService.$inject = ['$http', '$log'];
|
||||
|
||||
function ResetPasswordService($http, $log) {
|
||||
return resetPassword;
|
||||
function resetPassword(uuid, password) {
|
||||
return $http({
|
||||
method: 'POST',
|
||||
url: '/reset',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
transformRequest: function(obj) {
|
||||
var str = [];
|
||||
for(var p in obj)
|
||||
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
|
||||
return str.join("&");
|
||||
},
|
||||
data: {'reset_uuid': uuid, 'password': password}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
26
static/ng/resources/js/services/user/services.send-mail.js
Normal file
26
static/ng/resources/js/services/user/services.send-mail.js
Normal file
@ -0,0 +1,26 @@
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('harbor.services.user')
|
||||
.factory('SendMailService', SendMailService);
|
||||
|
||||
SendMailService.$inject = ['$http', '$log'];
|
||||
|
||||
function SendMailService($http, $log) {
|
||||
|
||||
return SendMail;
|
||||
|
||||
function SendMail(email) {
|
||||
return $http
|
||||
.get('/ng/sendEmail', {
|
||||
'params': {
|
||||
'email': email
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})();
|
@ -5,34 +5,30 @@
|
||||
angular
|
||||
.module('harbor.session')
|
||||
.controller('CurrentUserController', CurrentUserController)
|
||||
.directive('currentUser', currentUser);
|
||||
|
||||
CurrentUserController.$inject = ['CurrentUserService', '$log', '$window'];
|
||||
CurrentUserController.$inject = ['CurrentUserService', '$scope', '$timeout', '$window'];
|
||||
|
||||
function CurrentUserController(CurrentUserService, $log, $window) {
|
||||
function CurrentUserController(CurrentUserService, $scope, $timeout, $window) {
|
||||
|
||||
var vm = this;
|
||||
|
||||
CurrentUserService()
|
||||
.then(getCurrentUserComplete)
|
||||
.catch(getCurrentUserFailed);
|
||||
|
||||
function getCurrentUserComplete(data) {
|
||||
$log.info('login success');
|
||||
function getCurrentUserComplete(response) {
|
||||
console.log('Successful logged in.');
|
||||
$timeout(function(){
|
||||
$scope.$broadcast('currentUser', response.data);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function getCurrentUserFailed(e){
|
||||
if(e.status == 401) {
|
||||
$window.location = '/ng';
|
||||
}
|
||||
console.log('Have not logged in yet.');
|
||||
$timeout(function(){
|
||||
$scope.$broadcast('currentUser', null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function currentUser() {
|
||||
var directive = {
|
||||
restrict: 'A',
|
||||
controller: CurrentUserController,
|
||||
bindToController: true
|
||||
}
|
||||
return directive;
|
||||
}
|
||||
|
||||
})();
|
@ -13,4 +13,10 @@ func initNgRouters() {
|
||||
beego.Router("/ng/repository", &ng.RepositoryController{})
|
||||
beego.Router("/ng/sign_up", &ng.SignUpController{})
|
||||
beego.Router("/ng/account_setting", &ng.AccountSettingController{})
|
||||
beego.Router("/ng/forgot_password", &ng.ForgotPasswordController{})
|
||||
beego.Router("/ng/reset_password", &ng.ResetPasswordController{})
|
||||
|
||||
beego.Router("/ng/reset", &ng.CommonController{}, "post:ResetPassword")
|
||||
beego.Router("/ng/sendEmail", &ng.CommonController{}, "get:SendEmail")
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="container-fluid container-fluid-custom" current-user>
|
||||
<div class="container-fluid container-fluid-custom">
|
||||
<div class="container">
|
||||
<div class="row row-custom">
|
||||
<div class="col-xs-4 col-md-4">
|
||||
|
38
views/ng/forgot-password.htm
Normal file
38
views/ng/forgot-password.htm
Normal file
@ -0,0 +1,38 @@
|
||||
<div class="container-fluid container-fluid-custom" ng-controller="ForgotPasswordController as vm">
|
||||
<div class="container container-custom">
|
||||
<div class="row extend-height">
|
||||
<div class="section">
|
||||
<h1 class="col-md-12 col-md-offset-2 main-title title-color">Forgot Password</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-md-offset-2 main-content">
|
||||
<form name="form" class="form-horizontal" ng-submit="form.$valid && vm.sendMail(user)" >
|
||||
<div class="form-group">
|
||||
<label for="email" class="col-sm-3 control-label">Email:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="email" class="form-control" id="email" ng-model="user.email" ng-model-options="{ updateOn: 'blur' }" name="uEmail" required >
|
||||
<p class="help-block small-size-fonts">Please input the Email used when you signed up, a reset password Email will be sent to you.</p>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-message">
|
||||
<div ng-messages="form.uEmail.$error">
|
||||
<span ng-message="required">Email is required.</span>
|
||||
</div>
|
||||
<div ng-show="vm.hasError">
|
||||
<span>//vm.errorMessage//</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-8 col-md-10">
|
||||
<input type="submit" class="btn btn-success" value="Send">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -4,7 +4,7 @@
|
||||
|
||||
<title>{{.Title}}</title>
|
||||
</head>
|
||||
<body ng-app="harbor.app">
|
||||
<body ng-app="harbor.app" ng-controller="CurrentUserController as vm">
|
||||
{{.HeaderContent}}
|
||||
{{.LayoutContent}}
|
||||
{{.FooterContent}}
|
||||
|
21
views/ng/reset-password-mail.tpl
Normal file
21
views/ng/reset-password-mail.tpl
Normal file
@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<p>{{.Hint}}:</p>
|
||||
<a href="{{.URL}}/ng/reset_password?reset_uuid={{.UUID}}">{{.URL}}/ng/reset_password?reset_uuid={{.UUID}}</a>
|
||||
</body>
|
||||
</html>
|
48
views/ng/reset-password.htm
Normal file
48
views/ng/reset-password.htm
Normal file
@ -0,0 +1,48 @@
|
||||
<div class="container-fluid container-fluid-custom" ng-controller="ResetPasswordController as vm">
|
||||
<div class="container container-custom">
|
||||
<div class="row extend-height">
|
||||
<div class="section">
|
||||
<h1 class="col-md-12 col-md-offset-2 main-title title-color">Reset Password</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-md-offset-2 main-content">
|
||||
<form name="form" class="form-horizontal css-form" ng-submit="form.$valid && vm.resetPassword(user)" novalidate>
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-3 control-label">Password:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="password" class="form-control" id="password" ng-model="user.password" ng-model-options="{ updateOn: 'blur' }" name="uPassword" required password>
|
||||
<p class="help-block small-size-fonts">At least 7 characters with 1 lowercase letter, 1 capital letter and 1 numeric character.</p>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword" class="col-sm-3 control-label">Confirm Password:</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="password" class="form-control" id="confirmPassword" ng-model="user.confirmPassword" ng-model-options="{ updateOn: 'blur' }" name="uConfirmPassword" compare-to="user.password">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<span class="asterisk">*</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-7 col-md-10">
|
||||
<input type="submit" class="btn btn-default" ng-click="vm.cancel()" value="Cancel">
|
||||
<input type="submit" class="btn btn-primary" ng-disabled="form.$invalid" value="Save">
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-message">
|
||||
<div ng-messages="form.$dirty && form.uPassword.$error">
|
||||
<span ng-message="required">New password is required.</span>
|
||||
<span ng-message="password">New password is invalid.</span>
|
||||
</div>
|
||||
<div class="error-message" ng-messages="form.$dirty && form.uConfirmPassword.$error">
|
||||
<span ng-message="compareTo">Confirm password mismatch.</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
<nav class="navbar navbar-default navbar-custom" >
|
||||
<nav class="navbar navbar-default navbar-custom" ng-controller="HeaderController as vm">
|
||||
<div class="container container-custom">
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<div class="navbar-header">
|
||||
@ -9,8 +9,8 @@
|
||||
<a class="navbar-brand" href="#"><img class="img-responsive" src="/static/ng/resources/img/Harbor_Logo_rec.png" alt="Harbor's Logo"/></a>
|
||||
</div>
|
||||
<!-- Collect the nav links, forms, and other content for toggling -->
|
||||
<div class="collapse navbar-collapse" id="bs-harbor-navbar-collapse-1" ng-controller="HeaderController as vm">
|
||||
<optional-menu is-logged-in="vm.isLoggedIn" username="vm.currentUser.username"></optional-menu>
|
||||
<div class="collapse navbar-collapse" id="bs-harbor-navbar-collapse-1">
|
||||
<optional-menu></optional-menu>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<navigation-header></navigation-header>
|
||||
|
@ -54,6 +54,12 @@
|
||||
<script src="/static/ng/resources/js/layout/account-setting/account-setting.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/account-setting/account-setting.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/forgot-password/forgot-password.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/forgot-password/forgot-password.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/reset-password/reset-password.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/reset-password/reset-password.controller.js"></script>
|
||||
|
||||
<script src="/static/ng/resources/js/layout/index/index.module.js"></script>
|
||||
<script src="/static/ng/resources/js/layout/index/index.controller.js"></script>
|
||||
|
||||
@ -90,6 +96,9 @@
|
||||
<script src="/static/ng/resources/js/services/user/services.sign-up.js"></script>
|
||||
<script src="/static/ng/resources/js/services/user/services.user-exist.js"></script>
|
||||
<script src="/static/ng/resources/js/services/user/services.change-password.js"></script>
|
||||
<script src="/static/ng/resources/js/services/user/services.send-mail.js"></script>
|
||||
<script src="/static/ng/resources/js/services/user/services.reset-password.js"></script>
|
||||
|
||||
|
||||
<script src="/static/ng/resources/js/services/repository/services.repository.module.js"></script>
|
||||
<script src="/static/ng/resources/js/services/repository/services.list-repository.js"></script>
|
||||
|
Loading…
Reference in New Issue
Block a user