Merge remote-tracking branch 'upstream/job-service' into job-service

This commit is contained in:
Tan Jiang 2016-06-16 13:36:10 +08:00
commit a8f4a949d9
91 changed files with 3544 additions and 1557 deletions

View File

@ -1,3 +1,5 @@
sudo: true
language: go
go:
@ -5,36 +7,78 @@ go:
go_import_path: github.com/vmware/harbor
#service:
# - mysql
services:
- docker
- mysql
env: DB_HOST=127.0.0.1 DB_PORT=3306 DB_USR=root DB_PWD=
dist: trusty
addons:
apt:
packages:
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
env:
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_USR: root
DB_PWD:
DOCKER_COMPOSE_VERSION: 1.7.1
HARBOR_ADMIN: admin
HARBOR_ADMIN_PASSWD: Harbor12345
before_install:
- sudo ./tests/hostcfg.sh
- cd Deploy
- sudo ./prepare
- cd ..
install:
- sudo apt-get update && sudo apt-get install -y libldap2-dev
- sudo apt-get remove mysql-common mysql-server-5.5 mysql-server-core-5.5 mysql-client-5.5 mysql-client-core-5.5
- sudo apt-get autoremove
- sudo apt-get install libaio1
- wget -O mysql-5.6.14.deb http://dev.mysql.com/get/Downloads/MySQL-5.6/mysql-5.6.14-debian6.0-x86_64.deb/from/http://cdn.mysql.com/
- sudo dpkg -i mysql-5.6.14.deb
- sudo cp /opt/mysql/server-5.6/support-files/mysql.server /etc/init.d/mysql.server
- sudo ln -s /opt/mysql/server-5.6/bin/* /usr/bin/
- sudo sed -i'' 's/table_cache/table_open_cache/' /etc/mysql/my.cnf
- sudo sed -i'' 's/log_slow_queries/slow_query_log/' /etc/mysql/my.cnf
- sudo sed -i'' 's/basedir[^=]\+=.*$/basedir = \/opt\/mysql\/server-5.6/' /etc/mysql/my.cnf
- sudo /etc/init.d/mysql.server start
- mysql --version
# - sudo apt-get remove -y mysql-common mysql-server-5.5 mysql-server-core-5.5 mysql-client-5.5 mysql-client-core-5.5
# - sudo apt-get autoremove -y
# - sudo apt-get install -y libaio1
# - wget -O mysql-5.6.14.deb http://dev.mysql.com/get/Downloads/MySQL-5.6/mysql-5.6.14-debian6.0-x86_64.deb/from/http://cdn.mysql.com/
# - sudo dpkg -i mysql-5.6.14.deb
# - sudo cp /opt/mysql/server-5.6/support-files/mysql.server /etc/init.d/mysql.server
# - sudo ln -s /opt/mysql/server-5.6/bin/* /usr/bin/
# - sudo sed -i'' 's/table_cache/table_open_cache/' /etc/mysql/my.cnf
# - sudo sed -i'' 's/log_slow_queries/slow_query_log/' /etc/mysql/my.cnf
# - sudo sed -i'' 's/basedir[^=]\+=.*$/basedir = \/opt\/mysql\/server-5.6/' /etc/mysql/my.cnf
# - sudo /etc/init.d/mysql.server start
# - mysql --version
- go get -d github.com/docker/distribution
- go get -d github.com/docker/libtrust
- go get -d github.com/go-sql-driver/mysql
- go get github.com/golang/lint/golint
- go get github.com/GeertJohan/fgt
- sudo apt-get install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" docker-engine=1.11.1-0~trusty
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- sudo sed -i '$a DOCKER_OPTS=\"$DOCKER_OPTS --insecure-registry 127.0.0.1\"' /etc/default/docker
- sudo service docker restart
- go get github.com/dghubble/sling
- go get github.com/stretchr/testify
before_script:
# create tables and load data
- mysql < ./Deploy/db/registry.sql -uroot --verbose
script:
- go list ./... | grep -v /vendor/ | xargs -L1 fgt golint
- go list ./... | grep -v 'vendor' | xargs -L1 go vet
- go list ./... | grep -v 'vendor' | xargs -L1 go test -v
- go list ./... | grep -v 'tests' | grep -v /vendor/ | xargs -L1 fgt golint
- go list ./... | grep -v 'tests' | grep -v 'vendor' | xargs -L1 go vet
- go list ./... | grep -v 'tests' | grep -v 'vendor' | xargs -L1 go test -v
- docker-compose -f Deploy/docker-compose.yml up -d
- docker ps
- go run tests/startuptest.go http://localhost/
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}
# test for API
- sudo ./tests/testprepare.sh
- go test -v ./tests/apitests

View File

@ -5,6 +5,7 @@ Alexey Erkak <eryigin at mail.ru>
Allen Heavey <xheavey at gmail.com>
Amanda Zhang <amzhang at vmware.com>
Benniu Ji <benniuji at gmail.com>
Bin Liu <liubin0329 at gmail.com>
Bobby Zhang <junzhang at vmware.com>
Chaofeng Wu <chaofengw at vmware.com>
Daniel Jiang <jiangd at vmware.com>

View File

@ -2,8 +2,8 @@ appname = registry
runmode = dev
[lang]
types = en-US|zh-CN|de-DE|ru-RU
names = en-US|zh-CN|de-DE|ru-RU
types = en-US|zh-CN|de-DE|ru-RU|ja-JP
names = en-US|zh-CN|de-DE|ru-RU|ja-JP
[dev]
httpport = 80

View File

@ -13,7 +13,7 @@ Project Harbor is an enterprise-class registry server, which extends the open so
* **Graphical user portal**: User can easily browse, search Docker repositories, manage projects/namespaces.
* **AD/LDAP support**: Harbor integrates with existing enterprise AD/LDAP for user authentication and management.
* **Auditing**: All the operations to the repositories are tracked.
* **Internationalization**: Already localized for English, Chinese, German and Russian. More languages can be added.
* **Internationalization**: Already localized for English, Chinese, German, Japanese and Russian. More languages can be added.
* **RESTful API**: RESTful APIs for most administrative operations, easing intergration with external management platforms.
### Getting Started
@ -67,7 +67,7 @@ Harbor is available under the [Apache 2 license](LICENSE).
&nbsp; &nbsp; <a href="https://www.caicloud.io" border="0"><img alt="CaiCloud" src="docs/img/caicloudLogoWeb.png"></a>
### Users
<a href="https://www.madailicai.com/" border="0" target="_blank"><img alt="MaDaiLiCai" src="docs/img/UserMaDai.jpg"></a>
<a href="https://www.madailicai.com/" border="0" target="_blank"><img alt="MaDaiLiCai" src="docs/img/UserMaDai.jpg"></a> <a href="https://www.dianrong.com/" border="0" target="_blank"><img alt="Dianrong" src="docs/img/dianrong.png"></a>
### Supporting Technologies
<img alt="beego" src="docs/img/beegoLogo.png"> Harbor is powered by <a href="http://beego.me/">Beego</a>, an open source framework to build and develop applications in the Go way.

47
ROADMAP.md Normal file
View File

@ -0,0 +1,47 @@
## Harbor Roadmap
### About this document
This document provides description of items that are gathered from the community and planned in Harbor's roadmap. This should serve as a reference point for Harbor users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
### How to help?
Discussion on the roadmap can take place in threads under [Issues](https://github.com/vmware/harbor/issues). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
### How to add an item to the roadmap?
Please open an issue to track any initiative on the roadmap of Harbor. We will work with and rely on our community to focus our efforts to improve Harbor.
---
### 1. Image replication between Harbor instances
Enable images to be replicated between two or more Harbor instances. This is useful to have multiple registry servers servicing a large cluster of nodes, or have distributed registry instances with identical images.
### 2. Image deletion and garbage collection
a) Images can be deleted from UI. The files of deleted images are not removed immediately.
b) The files of deleted images are recycled by an administrator during system maintenance(Garbage collection). The registry service must be shut down during the process of garbage collection.
### 3. Authentication (OAuth2)
In addition to LDAP/AD and local users, OAuth 2.0 can be used to authenticate a user.
### 4. High Availability
Support multi-node deployment of Harbor for high availability, scalability and load-balancing purposes.
### 5. Statistics and description for repositories
User can add a description to a repository. The access count of a repo can be aggregated and displayed.
### 6. Audit all operations in the system
Currently only image related operations are logged. Other operations in Harbor, such as user creation/deletion, role changes, password reset, should be tracked as well.
### 7. Migration tool to move from an existing registry to Harbor
A tool to migrate images from a vanilla registry server to Harbor, without the need to export/import a large amount of data.
### 8. Support API versioning
Provide versioning of Harbor's API.

View File

@ -17,8 +17,11 @@ package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/astaxie/beego/validation"
"github.com/vmware/harbor/auth"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
@ -51,6 +54,30 @@ func (b *BaseAPI) DecodeJSONReq(v interface{}) {
}
}
// Validate validates v if it implements interface validation.ValidFormer
func (b *BaseAPI) Validate(v interface{}) {
validator := validation.Validation{}
isValid, err := validator.Valid(v)
if err != nil {
log.Errorf("failed to validate: %v", err)
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if !isValid {
message := ""
for _, e := range validator.Errors {
message += fmt.Sprintf("%s %s \n", e.Field, e.Message)
}
b.CustomAbort(http.StatusBadRequest, message)
}
}
// DecodeJSONReqAndValidate does both decoding and validation
func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) {
b.DecodeJSONReq(v)
b.Validate(v)
}
// ValidateUser checks if the request triggered by a valid user
func (b *BaseAPI) ValidateUser() int {
@ -94,3 +121,18 @@ func (b *BaseAPI) Redirect(statusCode int, resouceID string) {
b.Ctx.Redirect(statusCode, resoucreURI)
}
// GetIDFromURL checks the ID in request URL
func (b *BaseAPI) GetIDFromURL() int64 {
idStr := b.Ctx.Input.Param(":id")
if len(idStr) == 0 {
b.CustomAbort(http.StatusBadRequest, "invalid ID in URL")
}
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
b.CustomAbort(http.StatusBadRequest, "invalid ID in URL")
}
return id
}

86
api/log.go Normal file
View File

@ -0,0 +1,86 @@
/*
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.
*/
package api
import (
"net/http"
"strconv"
"time"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils/log"
)
//LogAPI handles request api/logs
type LogAPI struct {
BaseAPI
userID int
}
//Prepare validates the URL and the user
func (l *LogAPI) Prepare() {
l.userID = l.ValidateUser()
}
//Get returns the recent logs according to parameters
func (l *LogAPI) Get() {
var err error
startTime := l.GetString("start_time")
if len(startTime) != 0 {
i, err := strconv.ParseInt(startTime, 10, 64)
if err != nil {
log.Errorf("Parse startTime to int error, err: %v", err)
l.CustomAbort(http.StatusBadRequest, "startTime is not a valid integer")
}
startTime = time.Unix(i, 0).String()
}
endTime := l.GetString("end_time")
if len(endTime) != 0 {
j, err := strconv.ParseInt(endTime, 10, 64)
if err != nil {
log.Errorf("Parse endTime to int error, err: %v", err)
l.CustomAbort(http.StatusBadRequest, "endTime is not a valid integer")
}
endTime = time.Unix(j, 0).String()
}
var linesNum int
lines := l.GetString("lines")
if len(lines) != 0 {
linesNum, err = strconv.Atoi(lines)
if err != nil {
log.Errorf("Get parameters error--lines, err: %v", err)
l.CustomAbort(http.StatusBadRequest, "bad request of lines")
}
if linesNum <= 0 {
log.Warning("lines must be a positive integer")
l.CustomAbort(http.StatusBadRequest, "lines is 0 or negative")
}
} else if len(startTime) == 0 && len(endTime) == 0 {
linesNum = 10
}
var logList []models.AccessLog
logList, err = dao.GetRecentLogs(l.userID, linesNum, startTime, endTime)
if err != nil {
log.Errorf("Get recent logs error, err: %v", err)
l.CustomAbort(http.StatusInternalServerError, "Internal error")
}
l.Data["json"] = logList
l.ServeJSON()
}

View File

@ -33,7 +33,7 @@ type ProjectMemberAPI struct {
}
type memberReq struct {
Username string `json:"user_name"`
Username string `json:"username"`
UserID int `json:"user_id"`
Roles []int `json:"roles"`
}
@ -104,7 +104,7 @@ func (pma *ProjectMemberAPI) Get() {
log.Errorf("Error occurred in GetUser, error: %v", err)
pma.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
result["user_name"] = user.Username
result["username"] = user.Username
result["user_id"] = pma.memberID
result["roles"] = roleList
pma.Data["json"] = result

View File

@ -18,6 +18,7 @@ package api
import (
"fmt"
"net/http"
"strings"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
@ -40,6 +41,7 @@ type projectReq struct {
}
const projectNameMaxLen int = 30
const projectNameMinLen int = 4
// Prepare validates the URL and the user
func (p *ProjectAPI) Prepare() {
@ -75,7 +77,7 @@ func (p *ProjectAPI) Post() {
err := validateProjectReq(req)
if err != nil {
log.Errorf("Invalid project request, error: %v", err)
p.RenderError(http.StatusBadRequest, "Invalid request for creating project")
p.RenderError(http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err))
return
}
projectName := req.ProjectName
@ -197,10 +199,9 @@ func (p *ProjectAPI) List() {
p.ServeJSON()
}
// Put ...
func (p *ProjectAPI) Put() {
// ToggleProjectPublic ...
func (p *ProjectAPI) ToggleProjectPublic() {
p.userID = p.ValidateUser()
var req projectReq
var public int
@ -287,8 +288,16 @@ func validateProjectReq(req projectReq) error {
if len(pn) == 0 {
return fmt.Errorf("Project name can not be empty")
}
if len(pn) > projectNameMaxLen {
return fmt.Errorf("Project name is too long")
if isIllegalLength(req.ProjectName, projectNameMinLen, projectNameMaxLen) {
return fmt.Errorf("project name is illegal in length. (greater than 4 or less than 30)")
}
if isContainIllegalChar(req.ProjectName, []string{"~", "-", "$", "\\", "[", "]", "{", "}", "(", ")", "&", "^", "%", "*", "<", ">", "\"", "'", "/", "?", "@"}) {
return fmt.Errorf("project name contains illegal characters")
}
if pn != strings.ToLower(pn) {
return fmt.Errorf("project name must be in lower case")
}
return nil
}

View File

@ -14,12 +14,9 @@ import (
// RepPolicyAPI handles /api/replicationPolicies /api/replicationPolicies/:id/enablement
type RepPolicyAPI struct {
BaseAPI
policyID int64
policy *models.RepPolicy
}
// Prepare validates whether the user has system admin role
// and parsed the policy ID if it exists
func (pa *RepPolicyAPI) Prepare() {
uid := pa.ValidateUser()
var err error
@ -30,38 +27,45 @@ func (pa *RepPolicyAPI) Prepare() {
if !isAdmin {
pa.CustomAbort(http.StatusForbidden, "")
}
idStr := pa.Ctx.Input.Param(":id")
if len(idStr) > 0 {
pa.policyID, err = strconv.ParseInt(idStr, 10, 64)
if err != nil {
log.Errorf("Error parsing policy id: %s, error: %v", idStr, err)
pa.CustomAbort(http.StatusBadRequest, "invalid policy id")
}
p, err := dao.GetRepPolicy(pa.policyID)
if err != nil {
log.Errorf("Error occurred in GetRepPolicy, error: %v", err)
pa.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if p == nil {
pa.CustomAbort(http.StatusNotFound, fmt.Sprintf("policy does not exist, id: %v", pa.policyID))
}
pa.policy = p
}
}
// Get gets all the policies according to the project
// Get ...
func (pa *RepPolicyAPI) Get() {
projectID, err := pa.GetInt64("project_id")
id := pa.GetIDFromURL()
policy, err := dao.GetRepPolicy(id)
if err != nil {
log.Errorf("Failed to get project id, error: %v", err)
pa.RenderError(http.StatusBadRequest, "Invalid project id")
return
log.Errorf("failed to get policy %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
policies, err := dao.GetRepPolicyByProject(projectID)
if policy == nil {
pa.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
pa.Data["json"] = policy
pa.ServeJSON()
}
// List filters policies by name and project_id, if name and project_id
// are nil, List returns all policies
func (pa *RepPolicyAPI) List() {
name := pa.GetString("name")
projectIDStr := pa.GetString("project_id")
var projectID int64
var err error
if len(projectIDStr) != 0 {
projectID, err = strconv.ParseInt(projectIDStr, 10, 64)
if err != nil || projectID <= 0 {
pa.CustomAbort(http.StatusBadRequest, "invalid project ID")
}
}
policies, err := dao.FilterRepPolicies(name, projectID)
if err != nil {
log.Errorf("Failed to query policies from db, error: %v", err)
pa.RenderError(http.StatusInternalServerError, "Failed to query policies")
return
log.Errorf("failed to filter policies %s project ID %d: %v", name, projectID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
pa.Data["json"] = policies
pa.ServeJSON()
@ -69,9 +73,40 @@ func (pa *RepPolicyAPI) Get() {
// Post creates a policy, and if it is enbled, the replication will be triggered right now.
func (pa *RepPolicyAPI) Post() {
policy := models.RepPolicy{}
pa.DecodeJSONReq(&policy)
pid, err := dao.AddRepPolicy(policy)
policy := &models.RepPolicy{}
pa.DecodeJSONReqAndValidate(policy)
po, err := dao.GetRepPolicyByName(policy.Name)
if err != nil {
log.Errorf("failed to get policy %s: %v", policy.Name, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if po != nil {
pa.CustomAbort(http.StatusConflict, "name is already used")
}
project, err := dao.GetProjectByID(policy.ProjectID)
if err != nil {
log.Errorf("failed to get project %d: %v", policy.ProjectID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if project == nil {
pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("project %d does not exist", policy.ProjectID))
}
target, err := dao.GetRepTarget(policy.TargetID)
if err != nil {
log.Errorf("failed to get target %d: %v", policy.TargetID, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if target == nil {
pa.CustomAbort(http.StatusBadRequest, fmt.Sprintf("target %d does not exist", policy.TargetID))
}
pid, err := dao.AddRepPolicy(*policy)
if err != nil {
log.Errorf("Failed to add policy to DB, error: %v", err)
pa.RenderError(http.StatusInternalServerError, "Internal Error")
@ -91,12 +126,85 @@ func (pa *RepPolicyAPI) Post() {
pa.Redirect(http.StatusCreated, strconv.FormatInt(pid, 10))
}
// Put modifies name and description of policy
func (pa *RepPolicyAPI) Put() {
id := pa.GetIDFromURL()
originalPolicy, err := dao.GetRepPolicy(id)
if err != nil {
log.Errorf("failed to get policy %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if originalPolicy == nil {
pa.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
policy := &models.RepPolicy{}
pa.DecodeJSONReq(policy)
policy.ProjectID = originalPolicy.ProjectID
policy.TargetID = originalPolicy.TargetID
pa.Validate(policy)
if policy.Name != originalPolicy.Name {
po, err := dao.GetRepPolicyByName(policy.Name)
if err != nil {
log.Errorf("failed to get policy %s: %v", policy.Name, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if po != nil {
pa.CustomAbort(http.StatusConflict, "name is already used")
}
}
policy.ID = id
if err = dao.UpdateRepPolicy(policy); err != nil {
log.Errorf("failed to update policy %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if policy.Enabled == originalPolicy.Enabled {
return
}
//enablement has been modified
if policy.Enabled == 1 {
go func() {
if err := TriggerReplication(id, "", nil, models.RepOpTransfer); err != nil {
log.Errorf("failed to trigger replication of %d: %v", id, err)
} else {
log.Infof("replication of %d triggered", id)
}
}()
} else {
go func() {
if err := postReplicationAction(id, "stop"); err != nil {
log.Errorf("failed to stop replication of %d: %v", id, err)
} else {
log.Infof("try to stop replication of %d", id)
}
}()
}
}
type enablementReq struct {
Enabled int `json:"enabled"`
}
// UpdateEnablement changes the enablement of the policy
func (pa *RepPolicyAPI) UpdateEnablement() {
id := pa.GetIDFromURL()
policy, err := dao.GetRepPolicy(id)
if err != nil {
log.Errorf("failed to get policy %d: %v", id, err)
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if policy == nil {
pa.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
e := enablementReq{}
pa.DecodeJSONReq(&e)
if e.Enabled != 0 && e.Enabled != 1 {
@ -104,11 +212,11 @@ func (pa *RepPolicyAPI) UpdateEnablement() {
return
}
if pa.policy.Enabled == e.Enabled {
if policy.Enabled == e.Enabled {
return
}
if err := dao.UpdateRepPolicyEnablement(pa.policyID, e.Enabled); err != nil {
if err := dao.UpdateRepPolicyEnablement(id, e.Enabled); err != nil {
log.Errorf("Failed to update policy enablement in DB, error: %v", err)
pa.RenderError(http.StatusInternalServerError, "Internal Error")
return
@ -116,10 +224,18 @@ func (pa *RepPolicyAPI) UpdateEnablement() {
if e.Enabled == 1 {
go func() {
if err := TriggerReplication(pa.policyID, "", nil, models.RepOpTransfer); err != nil {
log.Errorf("failed to trigger replication of %d: %v", pa.policyID, err)
if err := TriggerReplication(id, "", nil, models.RepOpTransfer); err != nil {
log.Errorf("failed to trigger replication of %d: %v", id, err)
} else {
log.Infof("replication of %d triggered", pa.policyID)
log.Infof("replication of %d triggered", id)
}
}()
} else {
go func() {
if err := postReplicationAction(id, "stop"); err != nil {
log.Errorf("failed to stop replication of %d: %v", id, err)
} else {
log.Infof("try to stop replication of %d", id)
}
}()
}

View File

@ -133,6 +133,12 @@ func (ra *RepositoryAPI) Delete() {
log.Errorf("error occurred while listing tags of %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
// TODO remove the logic if the bug of registry is fixed
if len(tagList) == 0 {
ra.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
tags = append(tags, tagList...)
} else {
tags = append(tags, tag)
@ -294,3 +300,30 @@ func (ra *RepositoryAPI) getUsername() (string, error) {
return "", nil
}
//GetTopRepos handles request GET /api/repositories/top
func (ra *RepositoryAPI) GetTopRepos() {
var err error
var countNum int
count := ra.GetString("count")
if len(count) == 0 {
countNum = 10
} else {
countNum, err = strconv.Atoi(count)
if err != nil {
log.Errorf("Get parameters error--count, err: %v", err)
ra.CustomAbort(http.StatusBadRequest, "bad request of count")
}
if countNum <= 0 {
log.Warning("count must be a positive integer")
ra.CustomAbort(http.StatusBadRequest, "count is 0 or negative")
}
}
repos, err := dao.GetTopRepos(countNum)
if err != nil {
log.Errorf("error occured in get top 10 repos: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "internal server error")
}
ra.Data["json"] = repos
ra.ServeJSON()
}

View File

@ -120,23 +120,7 @@ func (t *TargetAPI) Ping() {
// Get ...
func (t *TargetAPI) Get() {
id := t.getIDFromURL()
// list targets
if id == 0 {
targets, err := dao.GetAllRepTargets()
if err != nil {
log.Errorf("failed to get all targets: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
for _, target := range targets {
target.Password = ""
}
t.Data["json"] = targets
t.ServeJSON()
return
}
id := t.GetIDFromURL()
target, err := dao.GetRepTarget(id)
if err != nil {
@ -148,6 +132,9 @@ func (t *TargetAPI) Get() {
t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
// The reason why the password is returned is that when user just wants to
// modify other fields of target he does not need to input the password again.
// The security issue can be fixed by enable https.
if len(target.Password) != 0 {
pwd, err := utils.ReversibleDecrypt(target.Password)
if err != nil {
@ -161,13 +148,46 @@ func (t *TargetAPI) Get() {
t.ServeJSON()
}
// List ...
func (t *TargetAPI) List() {
name := t.GetString("name")
targets, err := dao.FilterRepTargets(name)
if err != nil {
log.Errorf("failed to filter targets %s: %v", name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
for _, target := range targets {
if len(target.Password) == 0 {
continue
}
str, err := utils.ReversibleDecrypt(target.Password)
if err != nil {
log.Errorf("failed to decrypt password: %v", err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
target.Password = str
}
t.Data["json"] = targets
t.ServeJSON()
return
}
// Post ...
func (t *TargetAPI) Post() {
target := &models.RepTarget{}
t.DecodeJSONReq(target)
t.DecodeJSONReqAndValidate(target)
if len(target.Name) == 0 || len(target.URL) == 0 {
t.CustomAbort(http.StatusBadRequest, "name or URL is nil")
ta, err := dao.GetRepTargetByName(target.Name)
if err != nil {
log.Errorf("failed to get target %s: %v", target.Name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if ta != nil {
t.CustomAbort(http.StatusConflict, "name is already used")
}
if len(target.Password) != 0 {
@ -185,18 +205,35 @@ func (t *TargetAPI) Post() {
// Put ...
func (t *TargetAPI) Put() {
id := t.getIDFromURL()
if id == 0 {
t.CustomAbort(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
id := t.GetIDFromURL()
originalTarget, err := dao.GetRepTarget(id)
if err != nil {
log.Errorf("failed to get target %d: %v", id, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if originalTarget == nil {
t.CustomAbort(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
target := &models.RepTarget{}
t.DecodeJSONReq(target)
t.DecodeJSONReqAndValidate(target)
if target.ID == 0 {
target.ID = id
if target.Name != originalTarget.Name {
ta, err := dao.GetRepTargetByName(target.Name)
if err != nil {
log.Errorf("failed to get target %s: %v", target.Name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if ta != nil {
t.CustomAbort(http.StatusConflict, "name is already used")
}
}
target.ID = id
if len(target.Password) != 0 {
target.Password = utils.ReversibleEncrypt(target.Password)
}
@ -209,10 +246,7 @@ func (t *TargetAPI) Put() {
// Delete ...
func (t *TargetAPI) Delete() {
id := t.getIDFromURL()
if id == 0 {
t.CustomAbort(http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
}
id := t.GetIDFromURL()
target, err := dao.GetRepTarget(id)
if err != nil {
@ -229,17 +263,3 @@ func (t *TargetAPI) Delete() {
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}
func (t *TargetAPI) getIDFromURL() int64 {
idStr := t.Ctx.Input.Param(":id")
if len(idStr) == 0 {
return 0
}
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
t.CustomAbort(http.StatusBadRequest, "invalid ID in request URL")
}
return id
}

View File

@ -16,8 +16,10 @@
package api
import (
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
@ -133,14 +135,52 @@ func (ua *UserAPI) Get() {
}
// Put ...
func (ua *UserAPI) Put() { //currently only for toggle admin, so no request body
func (ua *UserAPI) Put() {
ldapAdminUser := (ua.AuthMode == "ldap_auth" && ua.userID == 1 && ua.userID == ua.currentUserID)
if !(ua.AuthMode == "db_auth" || ldapAdminUser) {
ua.CustomAbort(http.StatusForbidden, "")
}
if !ua.IsAdmin {
log.Warningf("current user, id: %d does not have admin role, can not update other user's role", ua.currentUserID)
ua.RenderError(http.StatusForbidden, "User does not have admin role")
if ua.userID != ua.currentUserID {
log.Warning("Guests can only change their own account.")
ua.CustomAbort(http.StatusForbidden, "Guests can only change their own account.")
}
}
user := models.User{UserID: ua.userID}
ua.DecodeJSONReq(&user)
err := commonValidate(user)
if err != nil {
log.Warning("Bad request in change user profile: %v", err)
ua.RenderError(http.StatusBadRequest, "change user profile error:"+err.Error())
return
}
userQuery := models.User{UserID: ua.userID}
dao.ToggleUserAdminRole(userQuery)
u, err := dao.GetUser(userQuery)
if err != nil {
log.Errorf("Error occurred in GetUser, error: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if u == nil {
log.Errorf("User with Id: %d does not exist", ua.userID)
ua.CustomAbort(http.StatusNotFound, "")
}
if u.Email != user.Email {
emailExist, err := dao.UserExists(user, "email")
if err != nil {
log.Errorf("Error occurred in change user profile: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if emailExist {
log.Warning("email has already been used!")
ua.RenderError(http.StatusConflict, "email has already been used!")
return
}
}
if err := dao.ChangeUserProfile(user); err != nil {
log.Errorf("Failed to update user profile, error: %v", err)
ua.CustomAbort(http.StatusInternalServerError, err.Error())
}
}
// Post ...
@ -157,12 +197,36 @@ func (ua *UserAPI) Post() {
user := models.User{}
ua.DecodeJSONReq(&user)
err := validate(user)
if err != nil {
log.Warning("Bad request in Register: %v", err)
ua.RenderError(http.StatusBadRequest, "register error:"+err.Error())
return
}
userExist, err := dao.UserExists(user, "username")
if err != nil {
log.Errorf("Error occurred in Register: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if userExist {
log.Warning("username has already been used!")
ua.RenderError(http.StatusConflict, "username has already been used!")
return
}
emailExist, err := dao.UserExists(user, "email")
if err != nil {
log.Errorf("Error occurred in change user profile: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
if emailExist {
log.Warning("email has already been used!")
ua.RenderError(http.StatusConflict, "email has already been used!")
return
}
userID, err := dao.Register(user)
if err != nil {
log.Errorf("Error occurred in Register: %v", err)
ua.RenderError(http.StatusInternalServerError, "Internal error.")
return
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10))
@ -186,9 +250,8 @@ func (ua *UserAPI) Delete() {
// ChangePassword handles PUT to /api/users/{}/password
func (ua *UserAPI) ChangePassword() {
ldapAdminUser := (ua.AuthMode == "ldap_auth" && ua.userID == 1 && ua.userID == ua.currentUserID)
if !(ua.AuthMode == "db_auth" || ldapAdminUser) {
ua.CustomAbort(http.StatusForbidden, "")
}
@ -228,3 +291,80 @@ func (ua *UserAPI) ChangePassword() {
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
}
// ToggleUserAdminRole handles PUT api/users/{}/toggleadmin
func (ua *UserAPI) ToggleUserAdminRole() {
if !ua.IsAdmin {
log.Warningf("current user, id: %d does not have admin role, can not update other user's role", ua.currentUserID)
ua.RenderError(http.StatusForbidden, "User does not have admin role")
return
}
userQuery := models.User{UserID: ua.userID}
ua.DecodeJSONReq(&userQuery)
if err := dao.ToggleUserAdminRole(userQuery.UserID, userQuery.HasAdminRole); err != nil {
log.Errorf("Error occurred in ToggleUserAdminRole: %v", err)
ua.CustomAbort(http.StatusInternalServerError, "Internal error.")
}
}
// validate only validate when user register
func validate(user models.User) error {
if isIllegalLength(user.Username, 0, 20) {
return fmt.Errorf("Username with illegal length.")
}
if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) {
return fmt.Errorf("Username contains illegal characters.")
}
if isIllegalLength(user.Password, 0, 20) {
return fmt.Errorf("Password with illegal length.")
}
if err := commonValidate(user); err != nil {
return err
}
return nil
}
//commonValidate validates email, realname, comment information when user register or change their profile
func commonValidate(user models.User) error {
if len(user.Email) > 0 {
if m, _ := 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,}))$`, user.Email); !m {
return fmt.Errorf("Email with illegal format.")
}
} else {
return fmt.Errorf("Email can't be empty")
}
if isIllegalLength(user.Realname, 0, 20) {
return fmt.Errorf("Realname with illegal length.")
}
if isContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) {
return fmt.Errorf("Realname contains illegal characters.")
}
if isIllegalLength(user.Comment, -1, 30) {
return fmt.Errorf("Comment with illegal length.")
}
return nil
}
func isIllegalLength(s string, min int, max int) bool {
if min == -1 {
return (len(s) > max)
}
if max == -1 {
return (len(s) <= min)
}
return (len(s) < min || len(s) > max)
}
func isContainIllegalChar(s string, illegalChar []string) bool {
for _, c := range illegalChar {
if strings.Index(s, c) >= 0 {
return true
}
}
return false
}

View File

@ -59,11 +59,13 @@ func listRoles(userID int, projectID int64) ([]models.Role, error) {
roles := make([]models.Role, 0, 1)
isSysAdmin, err := dao.IsAdminRole(userID)
if err != nil {
log.Errorf("failed to determine whether the user %d is system admin: %v", userID, err)
return roles, err
}
if isSysAdmin {
role, err := dao.GetRoleByID(models.PROJECTADMIN)
if err != nil {
log.Errorf("failed to get role %d: %v", models.PROJECTADMIN, err)
return roles, err
}
roles = append(roles, *role)
@ -72,6 +74,7 @@ func listRoles(userID int, projectID int64) ([]models.Role, error) {
rs, err := dao.GetUserProjectRoles(userID, projectID)
if err != nil {
log.Errorf("failed to get user %d 's roles for project %d: %v", userID, projectID, err)
return roles, err
}
roles = append(roles, rs...)
@ -167,26 +170,64 @@ func TriggerReplicationByRepository(repository string, tags []string, operation
}
}
func postReplicationAction(policyID int64, acton string) error {
data := struct {
PolicyID int64 `json:"policy_id"`
Action string `json:"action"`
}{
PolicyID: policyID,
Action: acton,
}
b, err := json.Marshal(&data)
if err != nil {
return err
}
url := buildReplicationActionURL()
resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(b))
if err != nil {
return err
}
if resp.StatusCode == http.StatusOK {
return nil
}
defer resp.Body.Close()
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("%d %s", resp.StatusCode, string(b))
}
func buildReplicationURL() string {
url := getJobServiceURL()
url = strings.TrimSpace(url)
url = strings.TrimRight(url, "/")
return fmt.Sprintf("%s/api/jobs/replication", url)
}
func buildJobLogURL(jobID string) string {
url := getJobServiceURL()
url = strings.TrimSpace(url)
url = strings.TrimRight(url, "/")
return fmt.Sprintf("%s/api/jobs/replication/%s/log", url, jobID)
}
func buildReplicationActionURL() string {
url := getJobServiceURL()
return fmt.Sprintf("%s/api/jobs/replication/actions", url)
}
func getJobServiceURL() string {
url := os.Getenv("JOB_SERVICE_URL")
url = strings.TrimSpace(url)
url = strings.TrimRight(url, "/")
if len(url) == 0 {
url = "http://jobservice"
}
return url
}

View File

@ -111,6 +111,9 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
u.Realname = m.Principal
u.Password = "12345678AbC"
u.Comment = "registered from LDAP."
if u.Email == "" {
u.Email = u.Username + "@placeholder.com"
}
userID, err := dao.Register(u)
if err != nil {
return nil, err

View File

@ -69,7 +69,7 @@ func (c *CommonController) Login() {
// SwitchLanguage handles UI request to switch between different languages and re-render template based on language.
func (c *CommonController) SwitchLanguage() {
lang := c.GetString("lang")
if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" || lang == "ru-RU" {
if lang == "en-US" || lang == "zh-CN" || lang == "de-DE" || lang == "ru-RU" || lang == "ja-JP" {
c.SetSession("lang", lang)
c.Data["Lang"] = lang
}

View File

@ -115,3 +115,49 @@ func AccessLog(username, projectName, repoName, repoTag, action string) error {
}
return err
}
//GetRecentLogs returns recent logs according to parameters
func GetRecentLogs(userID, linesNum int, startTime, endTime string) ([]models.AccessLog, error) {
var recentLogList []models.AccessLog
queryParam := make([]interface{}, 1)
sql := "select log_id, access_log.user_id, project_id, repo_name, repo_tag, GUID, operation, op_time, username from access_log left join user on access_log.user_id=user.user_id where project_id in (select distinct project_id from project_member where user_id = ?)"
queryParam = append(queryParam, userID)
if startTime != "" {
sql += " and op_time >= ?"
queryParam = append(queryParam, startTime)
}
if endTime != "" {
sql += " and op_time <= ?"
queryParam = append(queryParam, endTime)
}
sql += " order by op_time desc"
if linesNum != 0 {
sql += " limit ?"
queryParam = append(queryParam, linesNum)
}
o := GetOrmer()
_, err := o.Raw(sql, queryParam).QueryRows(&recentLogList)
if err != nil {
return nil, err
}
return recentLogList, nil
}
//GetTopRepos return top accessed public repos
func GetTopRepos(countNum int) ([]models.TopRepo, error) {
o := GetOrmer()
sql := "select repo_name, COUNT(repo_name) as access_count from access_log left join project on access_log.project_id=project.project_id where project.public = 1 and access_log.operation = 'pull' group by repo_name order by access_count desc limit ? "
queryParam := make([]interface{}, 1)
queryParam = append(queryParam, countNum)
var lists []models.TopRepo
_, err := o.Raw(sql, queryParam).QueryRows(&lists)
if err != nil {
return nil, err
}
return lists, nil
}

View File

@ -18,39 +18,18 @@ package dao
import (
"net"
"github.com/vmware/harbor/utils/log"
"os"
"strings"
"sync"
"time"
"github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql" //register mysql driver
"github.com/vmware/harbor/utils/log"
)
// NonExistUserID : if a user does not exist, the ID of the user will be 0.
const NonExistUserID = 0
func isIllegalLength(s string, min int, max int) bool {
if min == -1 {
return (len(s) > max)
}
if max == -1 {
return (len(s) <= min)
}
return (len(s) < min || len(s) > max)
}
func isContainIllegalChar(s string, illegalChar []string) bool {
for _, c := range illegalChar {
if strings.Index(s, c) >= 0 {
return true
}
}
return false
}
// GenerateRandomString generates a random string
func GenerateRandomString() (string, error) {
o := orm.NewOrm()

View File

@ -689,7 +689,7 @@ func TestDeleteProjectMember(t *testing.T) {
}
func TestToggleAdminRole(t *testing.T) {
err := ToggleUserAdminRole(*currentUser)
err := ToggleUserAdminRole(currentUser.UserID, 1)
if err != nil {
t.Errorf("Error in toggle ToggleUserAdmin role: %v, user: %+v", err, currentUser)
}
@ -700,7 +700,7 @@ func TestToggleAdminRole(t *testing.T) {
if !isAdmin {
t.Errorf("User is not admin after toggled, user id: %d", currentUser.UserID)
}
err = ToggleUserAdminRole(*currentUser)
err = ToggleUserAdminRole(currentUser.UserID, 0)
if err != nil {
t.Errorf("Error in toggle ToggleUserAdmin role: %v, user: %+v", err, currentUser)
}
@ -713,6 +713,39 @@ func TestToggleAdminRole(t *testing.T) {
}
}
func TestChangeUserProfile(t *testing.T) {
user := models.User{UserID: currentUser.UserID, Email: username + "@163.com", Realname: "test", Comment: "Unit Test"}
err := ChangeUserProfile(user)
if err != nil {
t.Errorf("Error occurred in ChangeUserProfile: %v", err)
}
loginedUser, err := GetUser(models.User{UserID: currentUser.UserID})
if err != nil {
t.Errorf("Error occurred in GetUser: %v", err)
}
if loginedUser != nil {
if loginedUser.Email != username+"@163.com" {
t.Errorf("user email does not update, expected: %s, acutal: %s", username+"@163.com", loginedUser.Email)
}
if loginedUser.Realname != "test" {
t.Errorf("user realname does not update, expected: %s, acutal: %s", "test", loginedUser.Realname)
}
if loginedUser.Comment != "Unit Test" {
t.Errorf("user email does not update, expected: %s, acutal: %s", "Unit Test", loginedUser.Comment)
}
}
}
func TestGetRecentLogs(t *testing.T) {
logs, err := GetRecentLogs(currentUser.UserID, 10, "2016-05-13 00:00:00", time.Now().String())
if err != nil {
t.Errorf("error occured in getting recent logs, error: %v", err)
}
if len(logs) <= 0 {
t.Errorf("get logs error, expected: %d, actual: %d", 1, len(logs))
}
}
func TestDeleteUser(t *testing.T) {
err := DeleteUser(currentUser.UserID)
if err != nil {
@ -731,6 +764,7 @@ var targetID, policyID, policyID2, policyID3, jobID, jobID2, jobID3 int64
func TestAddRepTarget(t *testing.T) {
target := models.RepTarget{
Name: "test",
URL: "127.0.0.1:5000",
Username: "admin",
Password: "admin",
@ -766,6 +800,83 @@ func TestAddRepTarget(t *testing.T) {
}
}
func TestGetRepTargetByName(t *testing.T) {
target, err := GetRepTarget(targetID)
if err != nil {
t.Fatalf("failed to get target %d: %v", targetID, err)
}
target2, err := GetRepTargetByName(target.Name)
if err != nil {
t.Fatalf("failed to get target %s: %v", target.Name, err)
}
if target.Name != target2.Name {
t.Errorf("unexpected target name: %s, expected: %s", target2.Name, target.Name)
}
}
func TestUpdateRepTarget(t *testing.T) {
target := &models.RepTarget{
Name: "name",
URL: "http://url",
Username: "username",
Password: "password",
}
id, err := AddRepTarget(*target)
if err != nil {
t.Fatalf("failed to add target: %v", err)
}
defer func() {
if err := DeleteRepTarget(id); err != nil {
t.Logf("failed to delete target %d: %v", id, err)
}
}()
target.ID = id
target.Name = "new_name"
target.URL = "http://new_url"
target.Username = "new_username"
target.Password = "new_password"
if err = UpdateRepTarget(*target); err != nil {
t.Fatalf("failed to update target: %v", err)
}
target, err = GetRepTarget(id)
if err != nil {
t.Fatalf("failed to get target %d: %v", id, err)
}
if target.Name != "new_name" {
t.Errorf("unexpected name: %s, expected: %s", target.Name, "new_name")
}
if target.URL != "http://new_url" {
t.Errorf("unexpected url: %s, expected: %s", target.URL, "http://new_url")
}
if target.Username != "new_username" {
t.Errorf("unexpected username: %s, expected: %s", target.Username, "new_username")
}
if target.Password != "new_password" {
t.Errorf("unexpected password: %s, expected: %s", target.Password, "new_password")
}
}
func TestFilterRepTargets(t *testing.T) {
targets, err := FilterRepTargets("test")
if err != nil {
t.Fatalf("failed to get all targets: %v", err)
}
if len(targets) == 0 {
t.Errorf("unexpected num of targets: %d, expected: %d", len(targets), 1)
}
}
func TestAddRepPolicy(t *testing.T) {
policy := models.RepPolicy{
ProjectID: 1,
@ -800,6 +911,23 @@ func TestAddRepPolicy(t *testing.T) {
}
func TestGetRepPolicyByName(t *testing.T) {
policy, err := GetRepPolicy(policyID)
if err != nil {
t.Fatalf("failed to get policy %d: %v", policyID, err)
}
policy2, err := GetRepPolicyByName(policy.Name)
if err != nil {
t.Fatalf("failed to get policy %s: %v", policy.Name, err)
}
if policy.Name != policy2.Name {
t.Errorf("unexpected name: %s, expected: %s", policy2.Name, policy.Name)
}
}
func TestDisableRepPolicy(t *testing.T) {
err := DisableRepPolicy(policyID)
if err != nil {
@ -1021,6 +1149,23 @@ func TestDeleteRepTarget(t *testing.T) {
}
}
func TestFilterRepPolicies(t *testing.T) {
_, err := FilterRepPolicies("name", 0)
if err != nil {
t.Fatalf("failed to filter policy")
}
}
func TestUpdateRepPolicy(t *testing.T) {
policy := &models.RepPolicy{
ID: policyID,
Name: "new_policy_name",
}
if err := UpdateRepPolicy(policy); err != nil {
t.Fatalf("failed to update policy")
}
}
func TestDeleteRepPolicy(t *testing.T) {
err := DeleteRepPolicy(policyID)
if err != nil {
@ -1029,7 +1174,7 @@ func TestDeleteRepPolicy(t *testing.T) {
}
t.Logf("delete rep policy, id: %d", policyID)
p, err := GetRepPolicy(policyID)
if err != nil {
if err != nil && err != orm.ErrNoRows {
t.Errorf("Error occured in GetRepPolicy:%v", err)
}
if p != nil {

View File

@ -18,7 +18,6 @@ package dao
import (
"github.com/vmware/harbor/models"
"errors"
"fmt"
"time"
@ -30,15 +29,7 @@ import (
// AddProject adds a project to the database along with project roles information and access log records.
func AddProject(project models.Project) (int64, error) {
if isIllegalLength(project.Name, 4, 30) {
return 0, errors.New("project name is illegal in length. (greater than 4 or less than 30)")
}
if isContainIllegalChar(project.Name, []string{"~", "-", "$", "\\", "[", "]", "{", "}", "(", ")", "&", "^", "%", "*", "<", ">", "\"", "'", "/", "?", "@"}) {
return 0, errors.New("project name contains illegal characters")
}
o := GetOrmer()
p, err := o.Raw("insert into project (owner_id, name, creation_time, update_time, deleted, public) values (?, ?, ?, ?, ?, ?)").Prepare()
if err != nil {
return 0, err

View File

@ -58,7 +58,7 @@ func DeleteProjectMember(projectID int64, userID int) error {
func GetUserByProject(projectID int64, queryUser models.User) ([]models.User, error) {
o := GetOrmer()
u := []models.User{}
sql := `select u.user_id, u.username, r.name rolename, r.role_id
sql := `select u.user_id, u.username, r.name rolename, r.role_id as role
from user u
join project_member pm
on pm.project_id = ? and u.user_id = pm.user_id

View File

@ -17,7 +17,6 @@ package dao
import (
"errors"
"regexp"
"time"
"github.com/vmware/harbor/models"
@ -26,14 +25,7 @@ import (
// Register is used for user to register, the password is encrypted before the record is inserted into database.
func Register(user models.User) (int64, error) {
err := validate(user)
if err != nil {
return 0, err
}
o := GetOrmer()
p, err := o.Raw("insert into user (username, password, realname, email, comment, salt, sysadmin_flag, creation_time, update_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").Prepare()
if err != nil {
return 0, err
@ -59,46 +51,6 @@ func Register(user models.User) (int64, error) {
return userID, nil
}
func validate(user models.User) error {
if isIllegalLength(user.Username, 0, 20) {
return errors.New("Username with illegal length.")
}
if isContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) {
return errors.New("Username contains illegal characters.")
}
if exist, _ := UserExists(models.User{Username: user.Username}, "username"); exist {
return errors.New("Username already exists.")
}
if len(user.Email) > 0 {
if m, _ := 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,}))$`, user.Email); !m {
return errors.New("Email with illegal format.")
}
if exist, _ := UserExists(models.User{Email: user.Email}, "email"); exist {
return errors.New("Email already exists.")
}
}
if isIllegalLength(user.Realname, 0, 20) {
return errors.New("Realname with illegal length.")
}
if isContainIllegalChar(user.Realname, []string{",", "~", "#", "$", "%"}) {
return errors.New("Realname contains illegal characters.")
}
if isIllegalLength(user.Password, 0, 20) {
return errors.New("Password with illegal length.")
}
if isIllegalLength(user.Comment, -1, 30) {
return errors.New("Comment with illegal length.")
}
return nil
}
// UserExists returns whether a user exists according username or Email.
func UserExists(user models.User, target string) (bool, error) {

View File

@ -11,13 +11,13 @@ import (
// AddRepTarget ...
func AddRepTarget(target models.RepTarget) (int64, error) {
o := orm.NewOrm()
o := GetOrmer()
return o.Insert(&target)
}
// GetRepTarget ...
func GetRepTarget(id int64) (*models.RepTarget, error) {
o := orm.NewOrm()
o := GetOrmer()
t := models.RepTarget{ID: id}
err := o.Read(&t)
if err == orm.ErrNoRows {
@ -26,37 +26,56 @@ func GetRepTarget(id int64) (*models.RepTarget, error) {
return &t, err
}
// GetRepTargetByName ...
func GetRepTargetByName(name string) (*models.RepTarget, error) {
o := GetOrmer()
t := models.RepTarget{Name: name}
err := o.Read(&t, "Name")
if err == orm.ErrNoRows {
return nil, nil
}
return &t, err
}
// DeleteRepTarget ...
func DeleteRepTarget(id int64) error {
o := orm.NewOrm()
o := GetOrmer()
_, err := o.Delete(&models.RepTarget{ID: id})
return err
}
// UpdateRepTarget ...
func UpdateRepTarget(target models.RepTarget) error {
o := orm.NewOrm()
if len(target.Password) != 0 {
_, err := o.Update(&target)
return err
}
_, err := o.Update(&target, "URL", "Name", "Username")
o := GetOrmer()
_, err := o.Update(&target, "URL", "Name", "Username", "Password")
return err
}
// GetAllRepTargets ...
func GetAllRepTargets() ([]*models.RepTarget, error) {
o := orm.NewOrm()
qs := o.QueryTable(&models.RepTarget{})
// FilterRepTargets filters targets by name
func FilterRepTargets(name string) ([]*models.RepTarget, error) {
o := GetOrmer()
var args []interface{}
sql := `select * from replication_target `
if len(name) != 0 {
sql += `where name like ? `
args = append(args, "%"+name+"%")
}
sql += `order by creation_time`
var targets []*models.RepTarget
_, err := qs.All(&targets)
return targets, err
if _, err := o.Raw(sql, args).QueryRows(&targets); err != nil {
return nil, err
}
return targets, nil
}
// AddRepPolicy ...
func AddRepPolicy(policy models.RepPolicy) (int64, error) {
o := orm.NewOrm()
o := GetOrmer()
sqlTpl := `insert into replication_policy (name, project_id, target_id, enabled, description, cron_str, start_time, creation_time, update_time ) values (?, ?, ?, ?, ?, ?, %s, NOW(), NOW())`
var sql string
if policy.Enabled == 1 {
@ -78,33 +97,103 @@ func AddRepPolicy(policy models.RepPolicy) (int64, error) {
// GetRepPolicy ...
func GetRepPolicy(id int64) (*models.RepPolicy, error) {
o := orm.NewOrm()
p := models.RepPolicy{ID: id}
err := o.Read(&p)
if err == orm.ErrNoRows {
return nil, nil
o := GetOrmer()
sql := `select * from replication_policy where id = ?`
var policy models.RepPolicy
if err := o.Raw(sql, id).QueryRow(&policy); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return &p, err
return &policy, nil
}
// FilterRepPolicies filters policies by name and project ID
func FilterRepPolicies(name string, projectID int64) ([]*models.RepPolicy, error) {
o := GetOrmer()
var args []interface{}
sql := `select rp.id, rp.project_id, p.name as project_name, rp.target_id,
rt.name as target_name, rp.name, rp.enabled, rp.description,
rp.cron_str, rp.start_time, rp.creation_time, rp.update_time
from replication_policy rp
join project p on rp.project_id=p.project_id
join replication_target rt on rp.target_id=rt.id `
if len(name) != 0 && projectID != 0 {
sql += `where rp.name like ? and rp.project_id = ? `
args = append(args, "%"+name+"%")
args = append(args, projectID)
} else if len(name) != 0 {
sql += `where rp.name like ? `
args = append(args, "%"+name+"%")
} else if projectID != 0 {
sql += `where rp.project_id = ? `
args = append(args, projectID)
}
sql += `order by rp.creation_time`
var policies []*models.RepPolicy
if _, err := o.Raw(sql, args).QueryRows(&policies); err != nil {
return nil, err
}
return policies, nil
}
// GetRepPolicyByName ...
func GetRepPolicyByName(name string) (*models.RepPolicy, error) {
o := GetOrmer()
sql := `select * from replication_policy where name = ?`
var policy models.RepPolicy
if err := o.Raw(sql, name).QueryRow(&policy); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return &policy, nil
}
// GetRepPolicyByProject ...
func GetRepPolicyByProject(projectID int64) ([]*models.RepPolicy, error) {
var res []*models.RepPolicy
o := orm.NewOrm()
_, err := o.QueryTable("replication_policy").Filter("project_id", projectID).All(&res)
return res, err
o := GetOrmer()
sql := `select * from replication_policy where project_id = ?`
var policies []*models.RepPolicy
if _, err := o.Raw(sql, projectID).QueryRows(&policies); err != nil {
return nil, err
}
return policies, nil
}
// UpdateRepPolicy ...
func UpdateRepPolicy(policy *models.RepPolicy) error {
o := GetOrmer()
_, err := o.Update(policy, "Name", "Enabled", "Description", "CronStr")
return err
}
// DeleteRepPolicy ...
func DeleteRepPolicy(id int64) error {
o := orm.NewOrm()
o := GetOrmer()
_, err := o.Delete(&models.RepPolicy{ID: id})
return err
}
// UpdateRepPolicyEnablement ...
func UpdateRepPolicyEnablement(id int64, enabled int) error {
o := orm.NewOrm()
o := GetOrmer()
p := models.RepPolicy{
ID: id,
Enabled: enabled}
@ -125,7 +214,7 @@ func DisableRepPolicy(id int64) error {
// AddRepJob ...
func AddRepJob(job models.RepJob) (int64, error) {
o := orm.NewOrm()
o := GetOrmer()
if len(job.Status) == 0 {
job.Status = models.JobPending
}
@ -137,7 +226,7 @@ func AddRepJob(job models.RepJob) (int64, error) {
// GetRepJob ...
func GetRepJob(id int64) (*models.RepJob, error) {
o := orm.NewOrm()
o := GetOrmer()
j := models.RepJob{ID: id}
err := o.Read(&j)
if err == orm.ErrNoRows {
@ -164,20 +253,20 @@ func GetRepJobToStop(policyID int64) ([]*models.RepJob, error) {
}
func repJobPolicyIDQs(policyID int64) orm.QuerySeter {
o := orm.NewOrm()
o := GetOrmer()
return o.QueryTable("replication_job").Filter("policy_id", policyID)
}
// DeleteRepJob ...
func DeleteRepJob(id int64) error {
o := orm.NewOrm()
o := GetOrmer()
_, err := o.Delete(&models.RepJob{ID: id})
return err
}
// UpdateRepJobStatus ...
func UpdateRepJobStatus(id int64, status string) error {
o := orm.NewOrm()
o := GetOrmer()
j := models.RepJob{
ID: id,
Status: status,

View File

@ -18,6 +18,7 @@ package dao
import (
"fmt"
"github.com/astaxie/beego/orm"
"github.com/vmware/harbor/models"
)
@ -83,6 +84,9 @@ func GetRoleByID(id int) (*models.Role, error) {
var role models.Role
if err := o.Raw(sql, id).QueryRow(&role); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return &role, nil

View File

@ -109,12 +109,13 @@ func ListUsers(query models.User) ([]models.User, error) {
}
// ToggleUserAdminRole gives a user admin role.
func ToggleUserAdminRole(u models.User) error {
func ToggleUserAdminRole(userID, hasAdmin int) error {
o := GetOrmer()
sql := `update user set sysadmin_flag =not sysadmin_flag where user_id = ?`
r, err := o.Raw(sql, u.UserID).Exec()
queryParams := make([]interface{}, 1)
sql := `update user set sysadmin_flag = ? where user_id = ?`
queryParams = append(queryParams, hasAdmin)
queryParams = append(queryParams, userID)
r, err := o.Raw(sql, queryParams).Exec()
if err != nil {
return err
}
@ -229,3 +230,13 @@ func DeleteUser(userID int) error {
_, err := o.Raw(`update user set deleted = 1 where user_id = ?`, userID).Exec()
return err
}
// ChangeUserProfile ...
func ChangeUserProfile(user models.User) error {
o := GetOrmer()
if _, err := o.Update(&user, "Email", "Realname", "Comment"); err != nil {
log.Errorf("update user failed, error: %v", err)
return err
}
return nil
}

BIN
docs/img/dianrong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -4,7 +4,7 @@ swagger: '2.0'
info:
title: Harbor API
description: These APIs provide services for manipulating Harbor project.
version: "0.1.0"
version: "0.1.1"
# the domain of the service
host: localhost
# array of all schemes that your API supports
@ -119,7 +119,7 @@ paths:
description: Project name already exists.
500:
description: Unexpected internal errors.
/projects/{project_id}:
/projects/{project_id}/publicity:
put:
summary: Update properties for a selected project.
description: |
@ -167,7 +167,7 @@ paths:
- name: access_log
in: body
schema:
$ref: '#/definitions/AccessLog'
$ref: '#/definitions/AccessLogFilter'
description: Search results of access logs.
tags:
- Products
@ -204,7 +204,7 @@ paths:
schema:
type: array
items:
$ref: '#/definitions/Role'
$ref: '#/definitions/User'
400:
description: Illegal format of provided ID value.
401:
@ -353,7 +353,24 @@ paths:
404:
description: Project ID does not exist.
500:
description: Unexpected internal errors.
description: Unexpected internal errors.
/statistics:
get:
summary: Get projects number and repositories number relevant to the user
description: |
This endpoint is aimed to statistic all of the projects number and repositories number relevant to the logined user, also the public projects number and repositories number. If the user is admin, he can also get total projects number and total repositories number.
tags:
- Products
responses:
200:
description: Get the projects number and repositories number relevant to the user successfully.
schema:
$ref: '#/definitions/StatisticMap'
401:
description: User need to log in first.
500:
description: Unexpected internal errors.
/users:
get:
summary: Get registered users of Harbor.
@ -407,10 +424,9 @@ paths:
description: Unexpected internal errors.
/users/{user_id}:
put:
summary: Update a registered user to change to be an administrator of Harbor.
summary: Update a registered user to change his profile.
description: |
This endpoint let a registered user change to be an administrator
of Harbor.
This endpoint let a registered user change his profile.
parameters:
- name: user_id
in: path
@ -418,6 +434,12 @@ paths:
format: int32
required: true
description: Registered user ID
- name: profile
in: body
description: Only email, realname and comment can be modified.
required: true
schema:
$ref: '#/definitions/User'
tags:
- Products
responses:
@ -490,7 +512,35 @@ paths:
403:
description: Guests can only change their own account.
500:
description: Unexpected internal errors.
description: Unexpected internal errors.
/users/{user_id}/sysadmin:
put:
summary: Update a registered user to change to be an administrator of Harbor.
description: |
This endpoint let a registered user change to be an administrator
of Harbor.
parameters:
- name: user_id
in: path
type: integer
format: int32
required: true
description: Registered user ID
tags:
- Products
responses:
200:
description: Updated user's admin role successfully.
400:
description: Invalid user ID.
401:
description: User need to log in first.
403:
description: User does not have permission of admin role.
404:
description: User ID does not exist.
500:
description: Unexpected internal errors.
/repositories:
get:
summary: Get repositories accompany with relevant project and repo name.
@ -517,7 +567,7 @@ paths:
schema:
type: array
items:
$ref: '#/definitions/Repository'
type: string
400:
description: Invalid project ID.
403:
@ -597,6 +647,70 @@ paths:
description: Retrieved manifests from a relevant repository successfully.
500:
description: Unexpected internal errors.
/repositories/top:
get:
summary: Get public repositories which are accessed most.
description: |
This endpoint aims to let users see the most popular public repositories
parameters:
- name: count
in: query
type: integer
format: int32
required: false
description: The number of the requested public repositories, default is 10 if not provided.
tags:
- Products
responses:
200:
description: Retrieved top repositories successfully.
schema:
type: array
items:
$ref: '#/definitions/TopRepo'
400:
description: Bad request because of invalid count.
500:
description: Unexpected internal errors.
/logs:
get:
summary: Get recent logs of the projects which the user is a member of
description: |
This endpoint let user see the recent operation logs of the projects which he is member of
parameters:
- name: lines
in: query
type: integer
format: int32
required: false
description: The number of logs to be shown, default is 10 if lines, start_time, end_time are not provided.
- name: start_time
in: query
type: integer
format: int64
required: false
description: The start time of logs to be shown in unix timestap
- name: end_time
in: query
type: integer
format: int64
required: false
description: The end time of logs to be shown in unix timestap
tags:
- Products
responses:
200:
description: Get the required logs successfully.
schema:
type: array
items:
$ref: '#/definitions/AccessLog'
400:
description: Bad request because of invalid parameter of lines or start_time or end_time.
401:
description: User need to login first.
500:
description: Unexpected internal errors.
definitions:
Search:
type: object
@ -605,12 +719,41 @@ definitions:
description: Search results of the projects that matched the filter keywords.
type: array
items:
$ref: '#/definitions/Project'
$ref: '#/definitions/SearchProject'
repositories:
description: Search results of the repositories that matched the filter keywords.
type: array
items:
$ref: '#/definitions/Repository'
$ref: '#/definitions/SearchRepository'
SearchProject:
type: object
properties:
id:
type: integer
format: int64
description: The ID of project
name:
type: string
description: The name of the project
public:
type: integer
format: int
description: The flag to indicate the publicity of the project (1 is public, 0 is non-public)
SearchRepository:
type: object
properties:
repository_name:
type: string
description: The name of the repository
project_name:
type: string
description: The name of the project that the repository belongs to
project_id:
type: integer
description: The ID of the project that the repository belongs to
project_public:
type: integer
description: The flag to indicate the publicity of the project that the repository belongs to (1 is public, 0 is not)
Project:
type: object
properties:
@ -622,30 +765,39 @@ definitions:
type: integer
format: int32
description: The owner ID of the project always means the creator of the project.
project_name:
name:
type: string
description: The name of the project.
creation_time:
type: string
description: The creation time of the project.
update_time:
type: string
description: The update time of the project.
deleted:
type: integer
format: int32
description: A deletion mark of the project.
description: A deletion mark of the project (1 means it's deleted, 0 is not)
user_id:
type: integer
format: int32
description: A relation field to the user table.
owner_name:
type: string
description: The owner name of tthe project always means the creator of the project.
description: The owner name of the project.
public:
type: boolean
format: boolean
description: The public status of the project.
togglable:
type: boolean
description: Correspond to the UI about showing the public status of the project.
description: Correspond to the UI about whether the project's publicity is updatable (for UI)
current_user_role_id:
type: integer
description: The role ID of the current user who triggered the API (for UI)
repo_count:
type: integer
description: The number of the repositories under this project.
Repository:
type: object
properties:
@ -680,6 +832,7 @@ definitions:
user_id:
type: integer
format: int32
description: The ID of the user.
username:
type: string
email:
@ -702,7 +855,7 @@ definitions:
new_password:
type: string
description: New password for marking as to be updated.
AccessLog:
AccessLogFilter:
type: object
properties:
username:
@ -711,14 +864,32 @@ definitions:
keywords:
type: string
description: Operation name specified when project created.
beginTimestamp:
begin_timestamp:
type: integer
format: int32
format: int64
description: Begin timestamp for querying access logs.
endTimestamp:
end_timestamp:
type: integer
format: int32
format: int64
description: End timestamp for querying accessl logs.
AccessLog:
type: object
properties:
log_id:
type: integer
description: The ID of the log entry.
repo_name:
type: string
description: Name of the repository in this log entry.
repo_tag:
type: string
description: Tag of the repository in this log entry.
operation:
type: string
description: The operation against the repository in this log entry.
op_time:
type: time
description: The time when this operation is triggered.
Role:
type: object
properties:
@ -741,6 +912,48 @@ definitions:
type: integer
format: int32
description: Role ID for updating project role member.
user_name:
username:
type: string
description: Username relevant to a project role member.
TopRepo:
type: object
properties:
repo_name:
type: string
description: The name of the repo
access_count:
type: integer
format: int
description: The access count of the repo
StatisticMap:
type: object
properties:
my_project_count:
type: integer
format: int32
description: The count of the projects which the user is a member of.
my_repo_count:
type: integer
format: int32
description: The count of the repositories belonging to the projects which the user is a member of.
public_project_count:
type: integer
format: int32
description: The count of the public projects.
public_repo_count:
type: integer
format: int32
description: The count of the public repositories belonging to the public projects which the user is a member of.
total_project_count:
type: integer
format: int32
description: The count of the total projects, only be seen when the is admin.
total_repo_count:
type: integer
format: int32
description: The count of the total repositories, only be seen when the user is admin.

View File

@ -1,54 +1,56 @@
# migration
Migration is a module for migrating database schema between different version of project [harbor](https://github.com/vmware/harbor)
# Migration guide
Migration is a module for migrating database schema between different version of project [Harbor](https://github.com/vmware/harbor)
This module is for those machine running Harbor's old version, such as 0.1.0. If your Harbor' version is up to date, please ignore this module.
**WARNING!!** You must backup your data before migrating
###installation
- step 1: modify migration.cfg
###Installation
- step 1: change `db_username`, `db_password`, `db_port`, `db_name` in migration.cfg
- step 2: build image from dockerfile
```
cd harbor-migration
docker build -t your-image-name .
docker build -t migrate-tool .
```
###migration operation
- show instruction of harbor-migration
```docker run your-image-name help```
- test mysql connection in harbor-migration
```docker run -v /data/database:/var/lib/mysql your-image-name test```
- create backup file in `/path/to/backup`
```
docker run -ti -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup your-image-name backup
```
- restore from backup file in `/path/to/backup`
```
docker run -ti -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup your-image-name restore
```
- perform database schema upgrade
```docker run -ti -v /data/database:/var/lib/mysql your-image-name up head```
you can use `-v /etc/localtime:/etc/localtime` to sync container timezone with host timezone.
you may change `/data/database` to the mysql volumes path you set in docker-compose.yml.
###migration step
- step 1: stop and remove harbor service
###Migrate Step
- step 1: stop and remove Harbor service
```
docker-compose down
```
- step 2: perform migration operation
- step 3: rebuild newest harbor images and restart service
- step 2: create backup file in `/path/to/backup`
```
docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool backup
```
- step 3: perform database schema upgrade
```docker run -ti --rm -v /data/database:/var/lib/mysql migrate-tool up head```
- step 4: rebuild newest Harbor images and restart service
```
docker-compose build && docker-compose up -d
```
You may change `/data/database` to the mysql volumes path you set in docker-compose.yml.
###Migration operation reference
- You can use `help` to show instruction of Harbor migration
```docker run migrate-tool help```
- You can use `test` to test mysql connection in Harbor migration
```docker run --rm -v /data/database:/var/lib/mysql migrate-tool test```
- You can restore from backup file in `/path/to/backup`
```
docker run -ti --rm -v /data/database:/var/lib/mysql -v /path/to/backup:/harbor-migration/backup migrate-tool restore
```

View File

@ -4,6 +4,7 @@
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.dialects import mysql
Base = declarative_base()
@ -20,8 +21,8 @@ class User(Base):
reset_uuid = sa.Column(sa.String(40))
salt = sa.Column(sa.String(40))
sysadmin_flag = sa.Column(sa.Integer)
creation_time = sa.Column(sa.DateTime)
update_time = sa.Column(sa.DateTime)
creation_time = sa.Column(mysql.TIMESTAMP)
update_time = sa.Column(mysql.TIMESTAMP)
class Properties(Base):
__tablename__ = 'properties'
@ -35,8 +36,8 @@ class ProjectMember(Base):
project_id = sa.Column(sa.Integer(), primary_key = True)
user_id = sa.Column(sa.Integer(), primary_key = True)
role = sa.Column(sa.Integer(), nullable = False)
creation_time = sa.Column(sa.DateTime(), nullable = True)
update_time = sa.Column(sa.DateTime(), nullable = True)
creation_time = sa.Column(mysql.TIMESTAMP, nullable = True)
update_time = sa.Column(mysql.TIMESTAMP, nullable = True)
sa.ForeignKeyConstraint(['project_id'], [u'project.project_id'], ),
sa.ForeignKeyConstraint(['role'], [u'role.role_id'], ),
sa.ForeignKeyConstraint(['user_id'], [u'user.user_id'], ),
@ -79,8 +80,8 @@ class Project(Base):
project_id = sa.Column(sa.Integer, primary_key=True)
owner_id = sa.Column(sa.ForeignKey(u'user.user_id'), nullable=False, index=True)
name = sa.Column(sa.String(30), nullable=False, unique=True)
creation_time = sa.Column(sa.DateTime)
update_time = sa.Column(sa.DateTime)
creation_time = sa.Column(mysql.TIMESTAMP)
update_time = sa.Column(mysql.TIMESTAMP)
deleted = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
public = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'"))
owner = relationship(u'User')

View File

@ -27,9 +27,10 @@ branch_labels = None
depends_on = None
from alembic import op
from datetime import datetime
from db_meta import *
from sqlalchemy.dialects import mysql
Session = sessionmaker()
def upgrade():
@ -44,12 +45,9 @@ def upgrade():
session.add(Properties(k='schema_version', v='0.1.1'))
#add column to table user
op.add_column('user', sa.Column('creation_time', sa.DateTime(), nullable=True))
op.add_column('user', sa.Column('creation_time', mysql.TIMESTAMP, nullable=True))
op.add_column('user', sa.Column('sysadmin_flag', sa.Integer(), nullable=True))
op.add_column('user', sa.Column('update_time', sa.DateTime(), nullable=True))
#fill update_time data into table user
session.query(User).update({User.update_time: datetime.now()})
op.add_column('user', sa.Column('update_time', mysql.TIMESTAMP, nullable=True))
#init all sysadmin_flag = 0
session.query(User).update({User.sysadmin_flag: 0})
@ -62,7 +60,7 @@ def upgrade():
for result in join_result:
session.add(ProjectMember(project_id=result.project_role.project_id, \
user_id=result.user_id, role=result.project_role.role_id, \
creation_time=datetime.now(), update_time=datetime.now()))
creation_time=None, update_time=None))
#update sysadmin_flag
sys_admin_result = session.query(UserProjectRole).\
@ -88,11 +86,9 @@ def upgrade():
session.delete(acc)
session.query(Access).update({Access.access_id: Access.access_id - 1})
#add column to table project
op.add_column('project', sa.Column('update_time', sa.DateTime(), nullable=True))
#add column to table project
op.add_column('project', sa.Column('update_time', mysql.TIMESTAMP, nullable=True))
#fill update_time data into table project
session.query(Project).update({Project.update_time: datetime.now()})
session.commit()
def downgrade():

View File

@ -21,19 +21,18 @@ import (
// AccessLog holds information about logs which are used to record the actions that user take to the resourses.
type AccessLog struct {
LogID int `orm:"column(log_id)" json:"LogId"`
UserID int `orm:"column(user_id)" json:"UserId"`
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`
RepoName string `orm:"column(repo_name)"`
RepoTag string `orm:"column(repo_tag)"`
GUID string `orm:"column(GUID)" json:"Guid"`
Operation string `orm:"column(operation)"`
OpTime time.Time `orm:"column(op_time)"`
Username string
Keywords string
LogID int `orm:"pk;column(log_id)" json:"log_id"`
UserID int `orm:"column(user_id)" json:"user_id"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
RepoName string `orm:"column(repo_name)" json:"repo_name"`
RepoTag string `orm:"column(repo_tag)" json:"repo_tag"`
GUID string `orm:"column(GUID)" json:"guid"`
Operation string `orm:"column(operation)" json:"operation"`
OpTime time.Time `orm:"column(op_time)" json:"op_time"`
Username string `json:"username"`
Keywords string `json:"keywords"`
BeginTime time.Time
BeginTimestamp int64
BeginTimestamp int64 `json:"begin_timestamp"`
EndTime time.Time
EndTimestamp int64
EndTimestamp int64 `json:"end_timestamp"`
}

View File

@ -7,5 +7,9 @@ import (
func init() {
orm.RegisterModel(new(RepTarget),
new(RepPolicy),
new(RepJob))
new(RepJob),
new(User),
new(Project),
new(Role),
new(AccessLog))
}

View File

@ -21,19 +21,19 @@ import (
// Project holds the details of a project.
type Project struct {
ProjectID int64 `orm:"column(project_id)" json:"ProjectId"`
OwnerID int `orm:"column(owner_id)" json:"OwnerId"`
Name string `orm:"column(name)"`
CreationTime time.Time `orm:"column(creation_time)"`
CreationTimeStr string
Deleted int `orm:"column(deleted)"`
UserID int `json:"UserId"`
OwnerName string
Public int `orm:"column(public)"`
ProjectID int64 `orm:"pk;column(project_id)" json:"project_id"`
OwnerID int `orm:"column(owner_id)" json:"owner_id"`
Name string `orm:"column(name)" json:"name"`
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
CreationTimeStr string `json:"creation_time_str"`
Deleted int `orm:"column(deleted)" json:"deleted"`
//UserID int `json:"UserId"`
OwnerName string `json:"owner_name"`
Public int `orm:"column(public)" json:"public"`
//This field does not have correspondent column in DB, this is just for UI to disable button
Togglable bool
UpdateTime time.Time `orm:"update_time" json:"update_time"`
Role int `json:"role_id"`
Role int `json:"current_user_role_id"`
RepoCount int `json:"repo_count"`
}

View File

@ -2,6 +2,8 @@ package models
import (
"time"
"github.com/astaxie/beego/validation"
)
const (
@ -29,10 +31,12 @@ const (
// RepPolicy is the model for a replication policy, which associate to a project and a target (destination)
type RepPolicy struct {
ID int64 `orm:"column(id)" json:"id"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
TargetID int64 `orm:"column(target_id)" json:"target_id"`
Name string `orm:"column(name)" json:"name"`
ID int64 `orm:"column(id)" json:"id"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
ProjectName string `json:"project_name,omitempty"`
TargetID int64 `orm:"column(target_id)" json:"target_id"`
TargetName string `json:"target_name,omitempty"`
Name string `orm:"column(name)" json:"name"`
// Target RepTarget `orm:"-" json:"target"`
Enabled int `orm:"column(enabled)" json:"enabled"`
Description string `orm:"column(description)" json:"description"`
@ -42,6 +46,33 @@ type RepPolicy struct {
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}
// Valid ...
func (r *RepPolicy) Valid(v *validation.Validation) {
if len(r.Name) == 0 {
v.SetError("name", "can not be empty")
}
if len(r.Name) > 256 {
v.SetError("name", "max length is 256")
}
if r.ProjectID <= 0 {
v.SetError("project_id", "invalid")
}
if r.TargetID <= 0 {
v.SetError("target_id", "invalid")
}
if r.Enabled != 0 && r.Enabled != 1 {
v.SetError("enabled", "must be 0 or 1")
}
if len(r.CronStr) > 256 {
v.SetError("cron_str", "max length is 256")
}
}
// RepJob is the model for a replication job, which is the execution unit on job service, currently it is used to transfer/remove
// a repository to/from a remote registry instance.
type RepJob struct {
@ -60,7 +91,7 @@ type RepJob struct {
// RepTarget is the model for a replication targe, i.e. destination, which wraps the endpoint URL and username/password of a remote registry.
type RepTarget struct {
ID int64 `orm:"column(id)" json:"id"`
URL string `orm:"column(url)" json:"url"`
URL string `orm:"column(url)" json:"endpoint"`
Name string `orm:"column(name)" json:"name"`
Username string `orm:"column(username)" json:"username"`
Password string `orm:"column(password)" json:"password"`
@ -68,17 +99,42 @@ type RepTarget struct {
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
}
// Valid ...
func (r *RepTarget) Valid(v *validation.Validation) {
if len(r.Name) == 0 {
v.SetError("name", "can not be empty")
}
if len(r.Name) > 64 {
v.SetError("name", "max length is 64")
}
if len(r.URL) == 0 {
v.SetError("endpoint", "can not be empty")
}
if len(r.URL) > 64 {
v.SetError("endpoint", "max length is 64")
}
// password is encoded using base64, the length of this field
// in DB is 64, so the max length in request is 48
if len(r.Password) > 48 {
v.SetError("password", "max length is 48")
}
}
//TableName is required by by beego orm to map RepTarget to table replication_target
func (rt *RepTarget) TableName() string {
func (r *RepTarget) TableName() string {
return "replication_target"
}
//TableName is required by by beego orm to map RepJob to table replication_job
func (rj *RepJob) TableName() string {
func (r *RepJob) TableName() string {
return "replication_job"
}
//TableName is required by by beego orm to map RepPolicy to table replication_policy
func (rp *RepPolicy) TableName() string {
func (r *RepPolicy) TableName() string {
return "replication_policy"
}

View File

@ -26,7 +26,7 @@ const (
// Role holds the details of a role.
type Role struct {
RoleID int `orm:"column(role_id)" json:"role_id"`
RoleID int `orm:"pk;column(role_id)" json:"role_id"`
RoleCode string `orm:"column(role_code)" json:"role_code"`
Name string `orm:"column(name)" json:"role_name"`

22
models/toprepo.go Normal file
View File

@ -0,0 +1,22 @@
/*
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.
*/
package models
// TopRepo holds information about repository that accessed most
type TopRepo struct {
RepoName string `json:"name"`
AccessCount int64 `json:"count"`
}

View File

@ -21,20 +21,21 @@ import (
// User holds the details of a user.
type User struct {
UserID int `orm:"column(user_id)" json:"UserId"`
Username string `orm:"column(username)" json:"username"`
Email string `orm:"column(email)" json:"email"`
Password string `orm:"column(password)" json:"password"`
Realname string `orm:"column(realname)" json:"realname"`
Comment string `orm:"column(comment)" json:"comment"`
Deleted int `orm:"column(deleted)"`
Rolename string
RoleID int `json:"RoleId"`
RoleList []Role
HasAdminRole int `orm:"column(sysadmin_flag)"`
ResetUUID string `orm:"column(reset_uuid)" json:"ResetUuid"`
Salt string `orm:"column(salt)"`
UserID int `orm:"pk;column(user_id)" json:"user_id"`
Username string `orm:"column(username)" json:"username"`
Email string `orm:"column(email)" json:"email"`
Password string `orm:"column(password)" json:"password"`
Realname string `orm:"column(realname)" json:"realname"`
Comment string `orm:"column(comment)" json:"comment"`
Deleted int `orm:"column(deleted)" json:"deleted"`
Rolename string `json:"role_name"`
//if this field is named as "RoleID", beego orm can not map role_id
//to it.
Role int `json:"role_id"`
// RoleList []Role `json:"role_list"`
HasAdminRole int `orm:"column(sysadmin_flag)" json:"has_admin_role"`
ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"`
Salt string `orm:"column(salt)"`
CreationTime time.Time `orm:"creation_time" json:"creation_time"`
UpdateTime time.Time `orm:"update_time" json:"update_time"`
}

View File

@ -38,7 +38,7 @@ type Handler struct {
// checkes the permission agains local DB and generates jwt token.
func (h *Handler) Get() {
var username string
var username, password string
request := h.Ctx.Request
service := h.GetString("service")
scopes := h.GetStrings("scope")
@ -49,7 +49,7 @@ func (h *Handler) Get() {
log.Debugf("Will grant all access as this request is from job service with legal secret.")
username = "job-service-user"
} else {
username, password, _ := request.BasicAuth()
username, password, _ = request.BasicAuth()
authenticated := authenticate(username, password)
if len(scopes) == 0 && !authenticated {

View File

@ -75,6 +75,7 @@ language_en-US = English
language_zh-CN = 中文
language_de-DE = Deutsch
language_ru-RU = Русский
language_ja-JP = 日本語
copyright = Copyright
all_rights_reserved = Alle Rechte vorbehalten.
index_desc = Project Harbor ist ein zuverlässiger Enterprise-Class Registry Server. Unternehmen können ihren eigenen Registry Server aufsetzen um die Produktivität und Sicherheit zu erhöhen. Project Harbor kann für Entwicklungs- wie auch Produktiv-Umgebungen genutzt werden.

View File

@ -76,6 +76,7 @@ language_en-US = English
language_zh-CN = 中文
language_de-DE = Deutsch
language_ru-RU = Русский
language_ja-JP = 日本語
copyright = Copyright
all_rights_reserved = All rights reserved.
index_desc = Project Harbor is to build an enterprise-class, reliable registry server. Enterprises can set up a private registry server in their own environment to improve productivity as well as security. Project Harbor can be used in both development and production environment.

View File

@ -0,0 +1,89 @@
page_title_index = Harbor
page_title_sign_in = ログイン - Harbor
page_title_project = プロジェクト - Harbor
page_title_item_details = 詳しい - Harbor
page_title_registration = 登録 - Harbor
page_title_add_user = ユーザを追加 - Harbor
page_title_forgot_password = パスワードを忘れました - Harbor
title_forgot_password = パスワードを忘れました
page_title_reset_password = パスワードをリセット - Harbor
title_reset_password = パスワードをリセット
page_title_change_password = パスワードを変更 - Harbor
title_change_password = パスワードを変更
page_title_search = サーチ - Harbor
sign_in = ログイン
sign_up = 登録
add_user = ユーザを追加
log_out = ログアウト
search_placeholder = プロジェクト名またはイメージ名
change_password = パスワードを変更
username_email = ユーザ名/メールアドレス
password = パスワード
forgot_password = パスワードを忘れました
welcome = ようこそ
my_projects = マイプロジェクト
public_projects = パブリックプロジェクト
admin_options = 管理者
project_name = プロジェクト名
creation_time = 作成日時
publicity = パブリック
add_project = プロジェクトを追加
check_for_publicity = パブリックプロジェクト
button_save = 保存する
button_cancel = 取り消しする
button_submit = 送信する
username = ユーザ名
email = メールアドレス
system_admin = システム管理者
dlg_button_ok = OK
dlg_button_cancel = 取り消し
registration = 登録
username_description = ログイン際に使うユーザ名を入力してください。
email_description = メールアドレスはパスワードをリセットする際に使われます。
full_name = フルネーム
full_name_description = フルネームを入力してください。
password_description = パスワード7英数字以上で、少なくとも 1小文字、 1大文字と 1数字でなければなりません。
confirm_password = パスワードを確認する
note_to_the_admin = メモ
old_password = 現在のパスワード
new_password = 新しいパスワード
forgot_password_description = ぱプロジェクトをリセットするメールはこのアドレスに送信します。
projects = プロジェクト
repositories = リポジトリ
search = サーチ
home = ホーム
project = プロジェクト
owner = オーナー
repo = リポジトリ
user = ユーザ
logs = ログ
repo_name = リポジトリ名
repo_tag = リポジトリタグ
add_members = メンバーを追加
operation = 操作
advance = さらに絞りこみで検索
all = 全部
others = その他
start_date = 開始日
end_date = 終了日
timestamp = タイムスタンプ
role = 役割
reset_email_hint = このリンクをクリックしてパスワードリセットの処理を続けてください
reset_email_subject = パスワードをリセットします
language = 日本語
language_en-US = English
language_zh-CN = 中文
language_de-DE = Deutsch
language_ru-RU = Русский
language_ja-JP = 日本語
copyright = コピーライト
all_rights_reserved = 無断複写・転載を禁じます
index_desc = Harborは、信頼性の高いエンタープライズクラスのRegistryサーバです。タープライズユーザはHarborを利用し、プライベートのRegistryサビースを構築し、生産性および安全性を向上させる事ができます。開発環境はもちろん、生産環境にも使用する事ができます。
index_desc_0 = 主な利点:
index_desc_1 = 1. セキュリティ: 知的財産権を組織内で確保する。
index_desc_2 = 2. 効率: プライベートなので、パブリックRegistryサビースにネットワーク通信が減らす。
index_desc_3 = 3. アクセス制御: ロールベースアクセス制御機能を実装し、更に既存のユーザ管理システムAD/LDAPと統合することも可能。
index_desc_4 = 4. 監査: すべてRegistryサビースへの操作が記録され、検査にに利用できる。
index_desc_5 = 5. 管理UI: 使いやすい管理UIが搭載する。
index_title = エンタープライズ Registry サビース

View File

@ -16,378 +16,441 @@ var global_messages = {
"username_is_required" : {
"en-US": "Username is required.",
"zh-CN": "用户名为必填项。",
"ja-JP": "ユーザ名は必須項目です。",
"de-DE": "Benutzername erforderlich.",
"ru-RU": "Требуется ввести имя пользователя."
},
"username_has_been_taken" : {
"en-US": "Username has been taken.",
"zh-CN": "用户名已被占用。",
"ja-JP": "ユーザ名はすでに登録されました。",
"de-DE": "Benutzername bereits vergeben.",
"ru-RU": "Имя пользователя уже используется."
},
"username_is_too_long" : {
"en-US": "Username is too long. (maximum 20 characters)",
"zh-CN": "用户名长度超出限制。最长为20个字符",
"ja-JP": "ユーザ名が長すぎです。20文字まで",
"de-DE": "Benutzername ist zu lang. (maximal 20 Zeichen)",
"ru-RU": "Имя пользователя слишком длинное. (максимум 20 символов)"
},
"username_contains_illegal_chars": {
"en-US": "Username contains illegal character(s).",
"zh-CN": "用户名包含不合法的字符。",
"ja-JP": "ユーザ名に使えない文字が入っています。",
"de-DE": "Benutzername enthält ungültige Zeichen.",
"ru-RU": "Имя пользователя содержит недопустимые символы."
},
"email_is_required" : {
"en-US": "Email is required.",
"zh-CN": "邮箱为必填项。",
"ja-JP": "メールアドレスが必須です。",
"de-DE": "E-Mail Adresse erforderlich.",
"ru-RU": "Требуется ввести E-mail адрес."
},
"email_contains_illegal_chars" : {
"en-US": "Email contains illegal character(s).",
"zh-CN": "邮箱包含不合法的字符。",
"ja-JP": "メールアドレスに使えない文字が入っています。",
"de-DE": "E-Mail Adresse enthält ungültige Zeichen.",
"ru-RU": "E-mail адрес содержит недопеустимые символы."
},
"email_has_been_taken" : {
"en-US": "Email has been taken.",
"zh-CN": "邮箱已被占用。",
"ja-JP": "メールアドレスがすでに使われました。",
"de-DE": "E-Mail Adresse wird bereits verwendet.",
"ru-RU": "Такой E-mail адрес уже используется."
},
"email_content_illegal" : {
"en-US": "Email format is illegal.",
"zh-CN": "邮箱格式不合法。",
"ja-JP": "メールアドレスフォーマットエラー。",
"de-DE": "Format der E-Mail Adresse ist ungültig.",
"ru-RU": "Недопустимый формат E-mail адреса."
},
"email_does_not_exist" : {
"en-US": "Email does not exist.",
"zh-CN": "邮箱不存在。",
"ja-JP": "メールアドレスが存在しません。",
"de-DE": "E-Mail Adresse existiert nicht.",
"ru-RU": "E-mail адрес не существует."
},
"realname_is_required" : {
"en-US": "Full name is required.",
"zh-CN": "全名为必填项。",
"ja-JP": "フルネームが必須です。",
"de-DE": "Vollständiger Name erforderlich.",
"ru-RU": "Требуется ввести полное имя."
},
"realname_is_too_long" : {
"en-US": "Full name is too long. (maximum 20 characters)",
"zh-CN": "全名长度超出限制。最长为20个字符",
"ja-JP": "フルネームは長すぎです。20文字まで",
"de-DE": "Vollständiger Name zu lang. (maximal 20 Zeichen)",
"ru-RU": "Полное имя слишком длинное. (максимум 20 символов)"
},
"realname_contains_illegal_chars" : {
"en-US": "Full name contains illegal character(s).",
"zh-CN": "全名包含不合法的字符。",
"ja-JP": "フルネームに使えない文字が入っています。",
"de-DE": "Vollständiger Name enthält ungültige Zeichen.",
"ru-RU": "Полное имя содержит недопустимые символы."
},
"password_is_required" : {
"en-US": "Password is required.",
"zh-CN": "密码为必填项。",
"ja-JP": "パスワードは必須です。",
"de-DE": "Passwort erforderlich.",
"ru-RU": "Требуется ввести пароль."
},
"password_is_invalid" : {
"en-US": "Password is invalid. At least 7 characters with 1 lowercase letter, 1 capital letter and 1 numeric character.",
"zh-CN": "密码无效。至少输入 7个字符且包含 1个小写字母1个大写字母和 1个数字。",
"ja-JP": "無効なパスワードです。7英数字以上で、 少なくとも1小文字、1大文字と1数字となります。",
"de-DE": "Passwort ungültig. Mindestens sieben Zeichen bestehend aus einem Kleinbuchstaben, einem Großbuchstaben und einer Zahl",
"ru-RU": "Такой пароль недопустим. Парольл должен содержать Минимум 7 символов, в которых будет присутствовать по меньшей мере 1 буква нижнего регистра, 1 буква верхнего регистра и 1 цифра"
},
"password_is_too_long" : {
"en-US": "Password is too long. (maximum 20 characters)",
"zh-CN": "密码长度超出限制。最长为20个字符",
"ja-JP": "パスワードは長すぎです。20文字まで",
"de-DE": "Passwort zu lang. (maximal 20 Zeichen)",
"ru-RU": "Пароль слишком длинный (максимум 20 символов)"
},
"password_does_not_match" : {
"en-US": "Passwords do not match.",
"zh-CN": "两次密码输入不一致。",
"ja-JP": "確認のパスワードが正しくありません。",
"de-DE": "Passwörter stimmen nicht überein.",
"ru-RU": "Пароли не совпадают."
},
"comment_is_too_long" : {
"en-US": "Comment is too long. (maximum 20 characters)",
"zh-CN": "备注长度超出限制。最长为20个字符",
"ja-JP": "コメントは長すぎです。20文字まで",
"de-DE": "Kommentar zu lang. (maximal 20 Zeichen)",
"ru-RU": "Комментарий слишком длинный. (максимум 20 символов)"
},
"comment_contains_illegal_chars" : {
"en-US": "Comment contains illegal character(s).",
"zh-CN": "备注包含不合法的字符。",
"ja-JP": "コメントに使えない文字が入っています。",
"de-DE": "Kommentar enthält ungültige Zeichen.",
"ru-RU": "Комментарий содержит недопустимые символы."
},
"project_name_is_required" : {
"en-US": "Project name is required.",
"zh-CN": "项目名称为必填项。",
"ja-JP": "プロジェクト名は必須です。",
"de-DE": "Projektname erforderlich.",
"ru-RU": "Необходимо ввести название Проекта."
},
"project_name_is_too_short" : {
"en-US": "Project name is too short. (minimum 4 characters)",
"zh-CN": "项目名称至少要求 4个字符。",
"ja-JP": "プロジェクト名は4文字以上です。",
"de-DE": "Projektname zu kurz. (mindestens 4 Zeichen)",
"ru-RU": "Название проекта слишком короткое. (миниму 4 символа)"
},
"project_name_is_too_long" : {
"en-US": "Project name is too long. (maximum 30 characters)",
"zh-CN": "项目名称长度超出限制。最长为30个字符",
"ja-JP": "プロジェクト名は長すぎです。30文字まで",
"de-DE": "Projektname zu lang. (maximal 30 Zeichen)",
"ru-RU": "Название проекта слишком длинное (максимум 30 символов)"
},
"project_name_contains_illegal_chars" : {
"en-US": "Project name contains illegal character(s).",
"zh-CN": "项目名称包含不合法的字符。",
"ja-JP": "プロジェクト名に使えない文字が入っています。",
"de-DE": "Projektname enthält ungültige Zeichen.",
"ru-RU": "Название проекта содержит недопустимые символы."
},
"project_exists" : {
"en-US": "Project exists.",
"zh-CN": "项目已存在。",
"ja-JP": "プロジェクトはすでに存在しました。",
"de-DE": "Projekt existiert bereits.",
"ru-RU": "Такой проект уже существует."
},
"delete_user" : {
"en-US": "Delete User",
"zh-CN": "删除用户",
"ja-JP": "ユーザを削除",
"de-DE": "Benutzer löschen",
"ru-RU": "Удалить пользователя"
},
"are_you_sure_to_delete_user" : {
"en-US": "Are you sure to delete ",
"zh-CN": "确认要删除用户 ",
"ja-JP": "ユーザを削除でよろしでしょうか ",
"de-DE": "Sind Sie sich sicher, dass Sie folgenden Benutzer löschen möchten: ",
"ru-RU": "Вы уверены что хотите удалить пользователя? "
},
"input_your_username_and_password" : {
"en-US": "Please input your username and password.",
"zh-CN": "请输入用户名和密码。",
"ja-JP": "ユーザ名とパスワードを入力してください。",
"de-DE": "Bitte geben Sie ihr Benutzername und Passwort ein.",
"ru-RU": "Введите имя пользователя и пароль."
},
"check_your_username_or_password" : {
"en-US": "Please check your username or password.",
"zh-CN": "请输入正确的用户名或密码。",
"ja-JP": "正しいユーザ名とパスワードを入力してください。",
"de-DE": "Bitte überprüfen Sie ihren Benutzernamen und Passwort.",
"ru-RU": "Проверьте свои имя пользователя и пароль."
},
"title_login_failed" : {
"en-US": "Login Failed",
"zh-CN": "登录失败",
"ja-JP": "ログインに失敗しました。",
"de-DE": "Anmeldung fehlgeschlagen",
"ru-RU": "Ошибка входа"
},
"title_change_password" : {
"en-US": "Change Password",
"zh-CN": "修改密码",
"ja-JP": "パスワードを変更します。",
"de-DE": "Passwort ändern",
"ru-RU": "Сменить пароль"
},
"change_password_successfully" : {
"en-US": "Password changed successfully.",
"zh-CN": "密码已修改。",
"ja-JP": "パスワードを変更しました。",
"de-DE": "Passwort erfolgreich geändert.",
"ru-RU": "Пароль успешно изменен."
},
"title_forgot_password" : {
"en-US": "Forgot Password",
"zh-CN": "忘记密码",
"ja-JP": "パスワードをリセットします。",
"de-DE": "Passwort vergessen",
"ru-RU": "Забыли пароль?"
},
"email_has_been_sent" : {
"en-US": "Email for resetting password has been sent.",
"zh-CN": "重置密码邮件已发送。",
"ja-JP": "パスワードをリセットするメールを送信しました。",
"de-DE": "Eine E-Mail mit einem Wiederherstellungslink wurde an Sie gesendet.",
"ru-RU": "На ваш E-mail было выслано письмо с инструкциями по сбросу пароля."
},
"send_email_failed" : {
"en-US": "Failed to send Email for resetting password.",
"zh-CN": "重置密码邮件发送失败。",
"ja-JP": "パスワードをリセットするメールを送信する際エラーが出ました",
"de-DE": "Fehler beim Senden der Wiederherstellungs-E-Mail.",
"ru-RU": "Ошибка отправки сообщения."
},
"please_login_first" : {
"en-US": "Please login first.",
"zh-CN": "请先登录。",
"ja-JP": "この先にログインが必要です。",
"de-DE": "Bitte melden Sie sich zuerst an.",
"ru-RU": "Сначала выполните вход в систему."
},
"old_password_is_not_correct" : {
"en-US": "Old password is not correct.",
"zh-CN": "原密码输入不正确。",
"ja-JP": "現在のパスワードが正しく入力されていません。",
"de-DE": "Altes Passwort ist nicht korrekt.",
"ru-RU": "Старый пароль введен неверно."
},
"please_input_new_password" : {
"en-US": "Please input new password.",
"zh-CN": "请输入新密码。",
"ja-JP": "あたらしいパスワードを入力してください",
"de-DE": "Bitte geben Sie ihr neues Passwort ein.",
"ru-RU": "Пожалуйста, введите новый пароль."
},
"invalid_reset_url": {
"en-US": "Invalid URL for resetting password.",
"zh-CN": "无效密码重置链接。",
"ja-JP": "無効なパスワードをリセットするリンク。",
"de-DE": "Ungültige URL zum Passwort wiederherstellen.",
"ru-RU": "Неверный URL для сброса пароля."
},
"reset_password_successfully" : {
"en-US": "Reset password successfully.",
"zh-CN": "密码重置成功。",
"ja-JP": "パスワードをリセットしました。",
"de-DE": "Passwort erfolgreich wiederhergestellt.",
"ru-RU": "Пароль успешно сброшен."
},
"internal_error": {
"en-US": "Internal error.",
"zh-CN": "内部错误,请联系系统管理员。",
"ja-JP": "エラーが出ました、管理者に連絡してください。",
"de-DE": "Interner Fehler.",
"ru-RU": "Внутренняя ошибка."
},
"title_reset_password" : {
"en-US": "Reset Password",
"zh-CN": "重置密码",
"ja-JP": "パスワードをリセットする",
"de-DE": "Passwort zurücksetzen",
"ru-RU": "Сбросить пароль"
},
"title_sign_up" : {
"en-US": "Sign Up",
"zh-CN": "注册",
"ja-JP": "登録",
"de-DE": "Registrieren",
"ru-RU": "Регистрация"
},
"title_add_user": {
"en-US": "Add User",
"zh-CN": "新增用户",
"ja-JP": "ユーザを追加",
"de-DE": "Benutzer hinzufügen",
"ru-RU": "Добавить пользователя"
},
"registered_successfully": {
"en-US": "Signed up successfully.",
"zh-CN": "注册成功。",
"ja-JP": "登録しました。",
"de-DE": "Erfolgreich registriert.",
"ru-RU": "Регистрация прошла успешно."
},
"registered_failed" : {
"en-US": "Failed to sign up.",
"zh-CN": "注册失败。",
"ja-JP": "登録でませんでした。",
"de-DE": "Registrierung fehlgeschlagen.",
"ru-RU": "Ошибка регистрации."
},
"added_user_successfully": {
"en-US": "Added user successfully.",
"zh-CN": "新增用户成功。",
"ja-JP": "ユーザを追加しました。",
"de-DE": "Benutzer erfolgreich erstellt.",
"ru-RU": "Пользователь успешно добавлен."
},
"added_user_failed": {
"en-US": "Adding user failed.",
"zh-CN": "新增用户失败。",
"ja-JP": "ユーザを追加できませんでした。",
"de-DE": "Benutzer erstellen fehlgeschlagen.",
"ru-RU": "Ошибка добавления пользователя."
},
"projects": {
"en-US": "Projects",
"zh-CN": "项目",
"ja-JP": "プロジェクト",
"de-DE": "Projekte",
"ru-RU": "Проекты"
},
"repositories" : {
"en-US": "Repositories",
"zh-CN": "镜像仓库",
"ja-JP": "リポジトリ",
"de-DE": "Repositories",
"ru-RU": "Репозитории"
},
"no_repo_exists" : {
"en-US": "No repositories found, please use 'docker push' to upload images.",
"zh-CN": "未发现镜像请用docker push命令上传镜像。",
"ja-JP": "イメージが見つかりませんでした。docker pushを利用しイメージをアップロードしてください。",
"de-DE": "Keine Repositories gefunden, bitte benutzen Sie 'docker push' um ein Image hochzuladen.",
"ru-RU": "Репозитории не найдены, используйте команду 'docker push' для добавления образов."
},
"tag" : {
"en-US": "Tag",
"zh-CN": "标签",
"ja-JP": "タグ",
"de-DE": "Tag",
"ru-RU": "Метка"
},
"pull_command": {
"en-US": "Pull Command",
"zh-CN": "Pull 命令",
"ja-JP": "Pull コマンド",
"de-DE": "Pull Befehl",
"ru-RU": "Команда для скачивания образа"
},
"image_details" : {
"en-US": "Image Details",
"zh-CN": "镜像详细信息",
"ja-JP": "イメージ詳細",
"de-DE": "Image Details",
"ru-RU": "Информация об образе"
},
"add_members" : {
"en-US": "Add Member",
"zh-CN": "添加成员",
"ja-JP": "メンバーを追加する",
"de-DE": "Mitglied hinzufügen",
"ru-RU": "Добавить Участника"
},
"edit_members" : {
"en-US": "Edit Members",
"zh-CN": "编辑成员",
"ja-JP": "メンバーを編集する",
"de-DE": "Mitglieder bearbeiten",
"ru-RU": "Редактировать Участников"
},
"add_member_failed" : {
"en-US": "Adding Member Failed",
"zh-CN": "添加成员失败",
"ja-JP": "メンバーを追加できません出した",
"de-DE": "Mitglied hinzufügen fehlgeschlagen",
"ru-RU": "Ошибка при добавлении нового участника"
},
"please_input_username" : {
"en-US": "Please input a username.",
"zh-CN": "请输入用户名。",
"ja-JP": "ユーザ名を入力してください。",
"de-DE": "Bitte geben Sie einen Benutzernamen ein.",
"ru-RU": "Пожалуйста, введите имя пользователя."
},
"please_assign_a_role_to_user" : {
"en-US": "Please assign a role to the user.",
"zh-CN": "请为用户分配角色。",
"ja-JP": "ユーザーに役割を割り当てるしてください。",
"de-DE": "Bitte weisen Sie dem Benutzer eine Rolle zu.",
"ru-RU": "Пожалуйста, назначьте роль пользователю."
},
"user_id_exists" : {
"en-US": "User is already a member.",
"zh-CN": "用户已经是成员。",
"ja-JP": "すでにメンバーに登録しました。",
"de-DE": "Benutzer ist bereits Mitglied.",
"ru-RU": "Пользователь уже является участником."
},
"user_id_does_not_exist" : {
"en-US": "User does not exist.",
"zh-CN": "不存在此用户。",
"ja-JP": "ユーザが見つかりませんでした。",
"de-DE": "Benutzer existiert nicht.",
"ru-RU": "Пользователя с таким именем не существует."
},
"insufficient_privileges" : {
"en-US": "Insufficient privileges.",
"zh-CN": "权限不足。",
"ja-JP": "権限エラー。",
"de-DE": "Unzureichende Berechtigungen.",
"ru-RU": "Недостаточно прав."
},
"operation_failed" : {
"en-US": "Operation Failed",
"zh-CN": "操作失败",
"ja-JP": "操作に失敗しました。",
"de-DE": "Befehl fehlgeschlagen",
"ru-RU": "Ошибка при выполнении данной операции"
},
"button_on" : {
"en-US": "On",
"zh-CN": "打开",
"ja-JP": "オン",
"de-DE": "An",
"ru-RU": "Вкл."
},
"button_off" : {
"en-US": "Off",
"zh-CN": "关闭",
"ja-JP": "オフ",
"de-DE": "Aus",
"ru-RU": "Откл."
}

View File

@ -76,6 +76,7 @@ language_en-US = English
language_zh-CN = 中文
language_de-DE = Deutsch
language_ru-RU = Русский
language_ja-JP = 日本語
copyright = Copyright
all_rights_reserved = Все права защищены.
index_desc = Проект Harbor представляет собой надежный сервер управления docker-образами корпоративного класса. Компании могут использовать данный сервер в своей инфарструктуе для повышения производительности и безопасности . Проект Harbor может использоваться как в среде разработки так и в продуктивной среде.

View File

@ -76,6 +76,7 @@ language_en-US = English
language_zh-CN = 中文
language_de-DE = Deutsch
language_ru-RU = Русский
language_ja-JP = 日本語
copyright = 版权所有
all_rights_reserved = 保留所有权利。
index_desc = Harbor是可靠的企业级Registry服务器。企业用户可使用Harbor搭建私有容器Registry服务提高生产效率和安全度既可应用于生产环境也可以在开发环境中使用。

View File

@ -13,7 +13,6 @@
limitations under the License.
*/
.footer {
margin-top: 60px;
width: 100%;
/* Set the fixed height of the footer here */
height: 60px;

View File

@ -24,7 +24,7 @@ jQuery(function(){
error: function(jqXhr){
if(jqXhr && jqXhr.status == 401){
document.location = "/signIn";
}
}
}
}).exec();
@ -36,12 +36,12 @@ jQuery(function(){
function bindEnterKey(){
$(document).on("keydown", function(e){
if(e.keyCode == 13){
e.preventDefault();
if($("#txtCommonSearch").is(":focus")){
document.location = "/search?q=" + $("#txtCommonSearch").val();
}else{
$("#btnSubmit").trigger("click");
}
e.preventDefault();
if($("#txtCommonSearch").is(":focus")){
document.location = "/search?q=" + $("#txtCommonSearch").val();
}else{
$("#btnSubmit").trigger("click");
}
}
});
}
@ -61,35 +61,35 @@ jQuery(function(){
type: "put",
data: {"old_password": oldPassword, "new_password" : password},
beforeSend: function(e){
unbindEnterKey();
$("h1").append(spinner.el);
$("#btnSubmit").prop("disabled", true);
unbindEnterKey();
$("h1").append(spinner.el);
$("#btnSubmit").prop("disabled", true);
},
complete: function(xhr, status){
spinner.stop();
$("#btnSubmit").prop("disabled", false);
if(xhr && xhr.status == 200){
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("title_change_password"),
"content": i18n.getMessage("change_password_successfully"),
"callback": function(){
window.close();
}
});
.dialogModal({
"title": i18n.getMessage("title_change_password"),
"content": i18n.getMessage("change_password_successfully"),
"callback": function(){
window.close();
}
});
}
},
error: function(jqXhr, status, error){
if(jqXhr && jqXhr.responseText.length){
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("title_change_password"),
"content": i18n.getMessage(jqXhr.responseText),
"callback": function(){
bindEnterKey();
return;
}
});
.dialogModal({
"title": i18n.getMessage("title_change_password"),
"content": i18n.getMessage(jqXhr.responseText),
"callback": function(){
bindEnterKey();
return;
}
});
}
}
}).exec();

View File

@ -12,8 +12,9 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
var AjaxUtil = function(params){
this.url = params.url;
this.data = params.data;
this.dataRaw = params.dataRaw;
@ -31,46 +32,47 @@ AjaxUtil.prototype.exec = function(){
var self = this;
return $.ajax({
url: self.url,
contentType: (self.dataRaw ? "application/x-www-form-urlencoded; charset=UTF-8" : "application/json; charset=utf-8"),
data: JSON.stringify(self.data) || self.dataRaw,
type: self.type,
dataType: "json",
success: function(data, status, xhr){
if(self.success != null){
self.success(data, status, xhr);
}
},
complete: function(jqXhr, status) {
if(self.complete != null){
self.complete(jqXhr, status);
}
},
error: function(jqXhr){
if(self.error != null){
self.error(jqXhr);
}else{
var errorMessage = self.errors[jqXhr.status] || jqXhr.responseText;
if(jqXhr.status == 401){
var lastUri = location.pathname + location.search;
if(lastUri != ""){
document.location = "/signIn?uri=" + encodeURIComponent(lastUri);
}else{
document.location = "/signIn";
url: self.url,
contentType: (self.dataRaw ? "application/x-www-form-urlencoded; charset=UTF-8" : "application/json; charset=utf-8"),
data: JSON.stringify(self.data) || self.dataRaw,
type: self.type,
dataType: "json",
success: function(data, status, xhr){
if(self.success != null){
self.success(data, status, xhr);
}
},
complete: function(jqXhr, status) {
if(self.complete != null){
self.complete(jqXhr, status);
}
},
error: function(jqXhr){
if(self.error != null){
self.error(jqXhr);
}else{
var errorMessage = self.errors[jqXhr.status] || jqXhr.responseText;
if(jqXhr.status == 401){
var lastUri = location.pathname + location.search;
if(lastUri != ""){
document.location = "/signIn?uri=" + encodeURIComponent(lastUri);
}else{
document.location = "/signIn";
}
}else if($.trim(errorMessage).length > 0){
$("#dlgModal").dialogModal({"title": i18n.getMessage("operation_failed"), "content": errorMessage});
}
}else if($.trim(errorMessage).length > 0){
$("#dlgModal").dialogModal({"title": i18n.getMessage("operation_failed"), "content": errorMessage});
}
}
}
});
});
};
var SUPPORT_LANGUAGES = {
"en-US": "English",
"zh-CN": "Chinese",
"de-DE": "German",
"ru-RU": "Russian"
"ru-RU": "Russian",
"ja-JP": "Japanese"
};
var DEFAULT_LANGUAGE = "en-US";
@ -133,7 +135,7 @@ jQuery(function(){
var self = this;
$("#dlgLabel", self).text(settings.title);
if(options.text){
$("#dlgBody", self).html(settings.content);
}else if(typeof settings.content == "object"){
@ -141,9 +143,9 @@ jQuery(function(){
var lines = ['<form class="form-horizontal">'];
for(var item in settings.content){
lines.push('<div class="form-group">'+
'<label class="col-sm-2 control-label">'+ item +'</label>' +
'<div class="col-sm-10"><p class="form-control-static">' + settings.content[item] + '</p></div>' +
'</div>');
'<label class="col-sm-2 control-label">'+ item +'</label>' +
'<div class="col-sm-10"><p class="form-control-static">' + settings.content[item] + '</p></div>' +
'</div>');
}
lines.push('</form>');
$("#dlgBody", self).html(lines.join(""));
@ -153,8 +155,13 @@ jQuery(function(){
}
if(settings.callback != null){
$("#dlgConfirm").on("click", function(){
settings.callback();
var hasEntered = false;
$("#dlgConfirm").on("click", function(e){
if(!hasEntered) {
hasEntered = true;
settings.callback();
}
});
}
$(self).modal('show');

View File

@ -13,26 +13,26 @@
limitations under the License.
*/
jQuery(function(){
$("#divErrMsg").css({"display": "none"});
validateOptions.Items = ["#EmailF"];
function bindEnterKey(){
$(document).on("keydown", function(e){
if(e.keyCode == 13){
e.preventDefault();
if($("#txtCommonSearch").is(":focus")){
document.location = "/search?q=" + $("#txtCommonSearch").val();
}else{
$("#btnSubmit").trigger("click");
}
e.preventDefault();
if($("#txtCommonSearch").is(":focus")){
document.location = "/search?q=" + $("#txtCommonSearch").val();
}else{
$("#btnSubmit").trigger("click");
}
}
});
}
function unbindEnterKey(){
$(document).off("keydown");
}
bindEnterKey();
bindEnterKey();
var spinner = new Spinner({scale:1}).spin();
$("#btnSubmit").on("click", function(){
@ -44,20 +44,20 @@ jQuery(function(){
"type": "get",
"data": {"username": username, "email": email},
"beforeSend": function(e){
unbindEnterKey();
$("h1").append(spinner.el);
$("#btnSubmit").prop("disabled", true);
unbindEnterKey();
$("h1").append(spinner.el);
$("#btnSubmit").prop("disabled", true);
},
"success": function(data, status, xhr){
if(xhr && xhr.status == 200){
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("title_forgot_password"),
"content": i18n.getMessage("email_has_been_sent"),
"callback": function(){
document.location="/";
}
});
.dialogModal({
"title": i18n.getMessage("title_forgot_password"),
"content": i18n.getMessage("email_has_been_sent"),
"callback": function(){
document.location="/";
}
});
}
},
@ -68,14 +68,14 @@ jQuery(function(){
"error": function(jqXhr, status, error){
if(jqXhr){
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("title_forgot_password"),
"content": i18n.getMessage(jqXhr.responseText),
"callback": function(){
bindEnterKey();
return;
}
});
.dialogModal({
"title": i18n.getMessage("title_forgot_password"),
"content": i18n.getMessage(jqXhr.responseText),
"callback": function(){
bindEnterKey();
return;
}
});
}
}
});

View File

@ -13,20 +13,20 @@
limitations under the License.
*/
jQuery(function(){
$("#btnSignUp").css({"visibility": "visible"});
$("#btnSignUp").css({"visibility": "visible"});
$(document).on("keydown", function(e){
if(e.keyCode == 13){
e.preventDefault();
if($("#txtCommonSearch").is(":focus")){
document.location = "/search?q=" + $("#txtCommonSearch").val();
document.location = "/search?q=" + $("#txtCommonSearch").val();
}
}
});
$("#btnSignIn").on("click", function(){
document.location = "/signIn";
});
$("#btnSignUp").on("click", function(){
$("#btnSignUp").on("click", function(){
document.location = "/register";
});
});

View File

@ -23,461 +23,451 @@ jQuery(function(){
if(jqXhr.status == 403){
return false;
}
}
}
}
}).exec()
).then(function(){
noNeedToLoginCallback();
needToLoginCallback();
}).fail(function(){
noNeedToLoginCallback();
});
function noNeedToLoginCallback(){
$("#tabItemDetail a:first").tab("show");
$("#btnFilterOption button:first").addClass("active");
$("#divErrMsg").hide();
if($("#public").val() == 1){
$("#tabItemDetail li:eq(1)").hide();
$("#tabItemDetail li:eq(2)").hide();
}
listRepo($("#repoName").val());
function listRepo(repoName){
).then(function(){
noNeedToLoginCallback();
needToLoginCallback();
}).fail(function(){
noNeedToLoginCallback();
});
function noNeedToLoginCallback(){
$("#tabItemDetail a:first").tab("show");
$("#btnFilterOption button:first").addClass("active");
$("#divErrMsg").hide();
if($("#public").val() == 1){
$("#tabItemDetail li:eq(1)").hide();
$("#tabItemDetail li:eq(2)").hide();
}
listRepo($("#repoName").val());
function listRepo(repoName){
$("#divErrMsg").hide();
new AjaxUtil({
url: "/api/repositories?project_id=" + $("#projectId").val() + "&q=" + repoName,
type: "get",
success: function(data, status, xhr){
if(xhr && xhr.status == 200){
$("#accordionRepo").children().remove();
if(data == null){
$("#divErrMsg").show();
$("#divErrMsg center").html(i18n.getMessage("no_repo_exists"));
return;
}
$.each(data, function(i, e){
var targetId = e.replace(/\//g, "------");
var row = '<div class="panel panel-default" targetId="' + targetId + '">' +
new AjaxUtil({
url: "/api/repositories?project_id=" + $("#projectId").val() + "&q=" + repoName,
type: "get",
success: function(data, status, xhr){
if(xhr && xhr.status == 200){
$("#accordionRepo").children().remove();
if(data == null){
$("#divErrMsg").show();
$("#divErrMsg center").html(i18n.getMessage("no_repo_exists"));
return;
}
$.each(data, function(i, e){
var targetId = e.replace(/\//g, "------").replace(/\./g, "---");
var row = '<div class="panel panel-default" targetId="' + targetId + '">' +
'<div class="panel-heading" role="tab" id="heading' + i + '"+ >' +
'<h4 class="panel-title">' +
'<a data-toggle="collapse" data-parent="#accordion" href="#collapse'+ i + '" aria-expanded="true" aria-controls="collapse' + i + '">' +
'<span class="list-group-item-heading"> <span class="glyphicon glyphicon-book blue"></span> ' + e + ' </span>' +
'</a>' +
'</h4>' +
'</div>' +
'<div id="collapse' + i + '" targetId="' + targetId + '" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading' + i + '">' +
'<div class="panel-body" id="' + targetId + '">' +
'<div class="table-responsive" style="height: auto;">' +
'<table class="table table-striped table-bordered table-condensed">' +
'<thead>' +
'<tr>' +
'<th class="st-sort-ascent" st-sort="name" st-sort-default=""><span class="glyphicon glyphicon-tag blue"></span> ' + i18n.getMessage("tag")+ ' </th>' +
'<th class="st-sort-ascent" st-sort="name" st-sort-default=""><span class="glyphicon glyphicon-tag blue"></span> ' + i18n.getMessage("pull_command") + ' </th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'</tbody>' +
'</table>'
'</div>' +
'</div>' +
'</div>' +
'</div>';
$("#accordionRepo").append(row);
});
if(repoName != ""){
$("#txtRepoName").val(repoName);
$("#accordionRepo #heading0 a").trigger("click");
'<h4 class="panel-title">' +
'<a data-toggle="collapse" data-parent="#accordion" href="#collapse'+ i + '" aria-expanded="true" aria-controls="collapse' + i + '">' +
'<span class="list-group-item-heading"> <span class="glyphicon glyphicon-book blue"></span> ' + e + ' </span>' +
'</a>' +
'</h4>' +
'</div>' +
'<div id="collapse' + i + '" targetId="' + targetId + '" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading' + i + '">' +
'<div class="panel-body" id="' + targetId + '">' +
'<div class="table-responsive" style="height: auto;">' +
'<table class="table table-striped table-bordered table-condensed">' +
'<thead>' +
'<tr>' +
'<th class="st-sort-ascent" st-sort="name" st-sort-default=""><span class="glyphicon glyphicon-tag blue"></span> ' + i18n.getMessage("tag")+ ' </th>' +
'<th class="st-sort-ascent" st-sort="name" st-sort-default=""><span class="glyphicon glyphicon-tag blue"></span> ' + i18n.getMessage("pull_command") + ' </th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'</tbody>' +
'</table>'
'</div>' +
'</div>' +
'</div>' +
'</div>';
$("#accordionRepo").append(row);
});
if(repoName != ""){
$("#txtRepoName").val(repoName);
$("#accordionRepo #heading0 a").trigger("click");
}
}
}
}
}).exec();
}
$("#btnSearchRepo").on("click", function(){
listRepo($.trim($("#txtRepoName").val()));
});
$('#accordionRepo').on('show.bs.collapse', function (e) {
$('#accordionRepo .in').collapse('hide');
var targetId = $(e.target).attr("targetId");
var repoName = targetId.replace(/------/g, "/");
new AjaxUtil({
url: "/api/repositories/tags?repo_name=" + repoName,
type: "get",
success: function(data, status, xhr){
$('#' + targetId +' table tbody tr').remove();
var row = [];
for(var i in data){
var tagName = data[i]
row.push('<tr><td><a href="#" imageId="' + tagName + '" repoName="' + repoName + '">' + tagName + '</a></td><td><input type="text" style="width:100%" readonly value=" docker pull '+ $("#harborRegUrl").val() +'/'+ repoName + ':' + tagName +'"></td></tr>');
}
$('#' + targetId +' table tbody').append(row.join(""));
$('#' + targetId +' table tbody tr a').on("click", function(e){
var imageId = $(this).attr("imageId");
var repoName = $(this).attr("repoName");
new AjaxUtil({
url: "/api/repositories/manifests?repo_name=" + repoName + "&tag=" + imageId,
type: "get",
success: function(data, status, xhr){
if(data){
for(var i in data){
if(data[i] == ""){
data[i] = "N/A";
}
}
data.Created = moment(new Date(data.Created)).format("YYYY-MM-DD HH:mm:ss");
$("#dlgModal").dialogModal({"title": i18n.getMessage("image_details"), "content": data});
}
}
}).exec();
});
}
}).exec();
});
}
function needToLoginCallback(){
var hasAuthorization = false;
$.when(
new AjaxUtil({
url: "/api/projects/" + $("#projectId").val() + "/members/current",
type: "get",
success: function(data, status, xhr){
if(xhr && xhr.status == 200 && data.roles != null && data.roles.length > 0){
hasAuthorization = true;
}
}
}).exec())
.done(function(){
if(!hasAuthorization) return false;
$("#tabItemDetail a:eq(1)").css({"visibility": "visible"});
$("#tabItemDetail a:eq(2)").css({"visibility": "visible"});
$(".glyphicon .glyphicon-pencil", "#tblUser").on("click", function(e){
$("#txtUserName").hide();
$("#lblUserName").show();
$("#dlgUserTitle").text(i18n.getMessage("edit_members"));
});
$("#btnAddUser").on("click", function(){
$("#operationType").val("add");
$("#spnSearch").show();
$("#txtUserName").prop("disabled", false)
$("#txtUserName").val("");
$("#lstRole input[name=chooseRole]:radio").prop("checked", false);
$("#dlgUserTitle").text(i18n.getMessage("add_members"));
});
$("#btnSave").on("click", function(){
var username = $("#txtUserName").val();
if($.trim(username).length == 0){
$("#dlgModal").dialogModal({"title": i18n.getMessage("add_member_failed"), "content": i18n.getMessage("please_input_username")});
return;
}
var projectId = $("#projectId").val();
var operationType = $("#operationType").val();
var userId = $("#editUserId").val();
var checkedRole = $("#lstRole input[name='chooseRole']:checked")
if(checkedRole.length == 0){
$("#dlgModal").dialogModal({"title": i18n.getMessage("add_member_failed"), "content": i18n.getMessage("please_assign_a_role_to_user")});
return;
}
var checkedRoleItemList = [];
$.each(checkedRole, function(i, e){
checkedRoleItemList.push(new Number($(this).val()));
});
var ajaxOpts = {};
if(operationType == "add"){
ajaxOpts.url = "/api/projects/" + projectId + "/members/";
ajaxOpts.type = "post";
ajaxOpts.data = {"roles" : checkedRoleItemList, "user_name": username};
}else if(operationType == "edit"){
ajaxOpts.url = "/api/projects/" + projectId + "/members/" + userId;
ajaxOpts.type = "put";
ajaxOpts.data = {"roles" : checkedRoleItemList};
}
new AjaxUtil({
url: ajaxOpts.url,
data: ajaxOpts.data,
type: ajaxOpts.type,
complete: function(jqXhr, status){
if(jqXhr && jqXhr.status == 200){
$("#btnCancel").trigger("click");
listUser(null);
}
},
errors: {
404: i18n.getMessage("user_id_does_not_exist"),
409: i18n.getMessage("user_id_exists"),
403: i18n.getMessage("insufficient_privileges")
}
}).exec();
});
var name_mapping = {
"projectAdmin": "Project Admin",
"developer": "Developer",
"guest": "Guest"
}
function listUserByProjectCallback(userList){
var loginedUserId = $("#userId").val();
var loginedUserRoleId = $("#roleId").val();
var ownerId = $("#ownerId").val();
$("#tblUser tbody tr").remove();
for(var i = 0; i < userList.length; ){
var userId = userList[i].UserId;
var roleId = userList[i].RoleId;
var username = userList[i].username;
var roleNameList = [];
for(var j = i; j < userList.length; i++, j++){
if(userList[j].UserId == userId){
roleNameList.push(name_mapping[userList[j].Rolename]);
}else{
break;
}
}
var row = '<tr>' +
'<td>' + username + '</td>' +
'<td>' + roleNameList.join(",") + '</td>' +
'<td>';
var isShowOperations = true;
if(loginedUserRoleId >= 3 /*role: developer guest*/){
isShowOperations = false;
}else if(ownerId == userId){
isShowOperations = false;
}else if (loginedUserId == userId){
isShowOperations = false;
}
if(isShowOperations){
row += '<a href="#" userid="' + userId + '" class="glyphicon glyphicon-pencil" data-toggle="modal" data-target="#dlgUser"></a>&nbsp;' +
'<a href="#" userid="' + userId + '" roleid="' + roleId + '" class="glyphicon glyphicon-trash"></a>';
}
row += '</td></tr>';
$("#tblUser tbody").append(row);
}
}
function searchAccessLogCallback(LogList){
$("#tabOperationLog tbody tr").remove();
$.each(LogList || [], function(i, e){
$("#tabOperationLog tbody").append(
'<tr>' +
'<td>' + e.Username + '</td>' +
'<td>' + e.RepoName + '</td>' +
'<td>' + e.RepoTag + '</td>' +
'<td>' + e.Operation + '</td>' +
'<td>' + moment(new Date(e.OpTime)).format("YYYY-MM-DD HH:mm:ss") + '</td>' +
'</tr>');
});
}
function getUserRoleCallback(userId){
new AjaxUtil({
url: "/api/projects/" + $("#projectId").val() + "/members/" + userId,
type: "get",
success: function(data, status, xhr){
var user = data;
$("#operationType").val("edit");
$("#editUserId").val(user.user_id);
$("#spnSearch").hide();
$("#txtUserName").val(user.user_name);
$("#txtUserName").prop("disabled", true);
$("#btnSave").removeClass("disabled");
$("#dlgUserTitle").text(i18n.getMessage("edit_members"));
$("#lstRole input[name=chooseRole]:radio").not('[value=' + user.role_id + ']').prop("checked", false)
$.each(user.roles, function(i, e){
$("#lstRole input[name=chooseRole]:radio").filter('[value=' + e.role_id + ']').prop("checked", "checked");
});
}
}).exec();
}
function listUser(username){
$.when(
new AjaxUtil({
url: "/api/projects/" + $("#projectId").val() + "/members?username=" + (username == null ? "" : username),
type: "get",
errors: {
403: ""
},
success: function(data, status, xhr){
return data || [];
}
}).exec()
).done(function(userList){
listUserByProjectCallback(userList || []);
$("#tblUser .glyphicon-pencil").on("click", function(e){
var userId = $(this).attr("userid")
getUserRoleCallback(userId);
});
$("#tblUser .glyphicon-trash").on("click", function(){
var userId = $(this).attr("userid");
new AjaxUtil({
url: "/api/projects/" + $("#projectId").val() + "/members/" + userId,
type: "delete",
complete: function(jqXhr, status){
if(jqXhr && jqXhr.status == 200){
listUser(null);
}
}
}).exec();
});
});
}
listUser(null);
listOperationLogs();
function listOperationLogs(){
var projectId = $("#projectId").val();
$.when(
new AjaxUtil({
url : "/api/projects/" + projectId + "/logs/filter",
data: {},
type: "post",
success: function(data){
return data || [];
}
}).exec()
).done(function(operationLogs){
searchAccessLogCallback(operationLogs);
});
}
$("#btnSearchUser").on("click", function(){
var username = $("#txtSearchUser").val();
if($.trim(username).length == 0){
username = null;
}
listUser(username);
$("#btnSearchRepo").on("click", function(){
listRepo($.trim($("#txtRepoName").val()));
});
function toUTCSeconds(date, hour, min, sec) {
var t = new Date(date);
t.setHours(hour);
t.setMinutes(min);
t.setSeconds(sec);
var utcTime = new Date(t.getUTCFullYear(),
$('#accordionRepo').on('show.bs.collapse', function (e) {
$('#accordionRepo .in').collapse('hide');
var targetId = $(e.target).attr("targetId");
var repoName = targetId.replace(/[-]{6}/g, "/").replace(/[-]{3}/g, ".");
new AjaxUtil({
url: "/api/repositories/tags?repo_name=" + repoName,
type: "get",
success: function(data, status, xhr){
$('#' + targetId +' table tbody tr').remove();
var row = [];
for(var i in data){
var tagName = data[i]
row.push('<tr><td><a href="#" imageId="' + tagName + '" repoName="' + repoName + '">' + tagName + '</a></td><td><input type="text" style="width:100%" readonly value=" docker pull '+ $("#harborRegUrl").val() +'/'+ repoName + ':' + tagName +'"></td></tr>');
}
$('#' + targetId +' table tbody').append(row.join(""));
$('#' + targetId +' table tbody tr a').on("click", function(e){
var imageId = $(this).attr("imageId");
var repoName = $(this).attr("repoName");
new AjaxUtil({
url: "/api/repositories/manifests?repo_name=" + repoName + "&tag=" + imageId,
type: "get",
success: function(data, status, xhr){
if(data){
for(var i in data){
if(data[i] == ""){
data[i] = "N/A";
}
}
data.Created = moment(new Date(data.Created)).format("YYYY-MM-DD HH:mm:ss");
$("#dlgModal").dialogModal({"title": i18n.getMessage("image_details"), "content": data});
}
}
}).exec();
});
}
}).exec();
});
}
function needToLoginCallback(){
var hasAuthorization = false;
$.when(
new AjaxUtil({
url: "/api/projects/" + $("#projectId").val() + "/members/current",
type: "get",
success: function(data, status, xhr){
if(xhr && xhr.status == 200 && data.roles != null && data.roles.length > 0){
hasAuthorization = true;
}
}
}).exec())
.done(function(){
if(!hasAuthorization) return false;
$("#tabItemDetail a:eq(1)").css({"visibility": "visible"});
$("#tabItemDetail a:eq(2)").css({"visibility": "visible"});
$(".glyphicon .glyphicon-pencil", "#tblUser").on("click", function(e){
$("#txtUserName").hide();
$("#lblUserName").show();
$("#dlgUserTitle").text(i18n.getMessage("edit_members"));
});
$("#btnAddUser").on("click", function(){
$("#operationType").val("add");
$("#spnSearch").show();
$("#txtUserName").prop("disabled", false)
$("#txtUserName").val("");
$("#lstRole input[name=chooseRole]:radio").prop("checked", false);
$("#dlgUserTitle").text(i18n.getMessage("add_members"));
});
$("#btnSave").on("click", function(){
var username = $("#txtUserName").val();
if($.trim(username).length == 0){
$("#dlgModal").dialogModal({"title": i18n.getMessage("add_member_failed"), "content": i18n.getMessage("please_input_username")});
return;
}
var projectId = $("#projectId").val();
var operationType = $("#operationType").val();
var userId = $("#editUserId").val();
var checkedRole = $("#lstRole input[name='chooseRole']:checked")
if(checkedRole.length == 0){
$("#dlgModal").dialogModal({"title": i18n.getMessage("add_member_failed"), "content": i18n.getMessage("please_assign_a_role_to_user")});
return;
}
var checkedRoleItemList = [];
$.each(checkedRole, function(i, e){
checkedRoleItemList.push(new Number($(this).val()));
});
var ajaxOpts = {};
if(operationType == "add"){
ajaxOpts.url = "/api/projects/" + projectId + "/members/";
ajaxOpts.type = "post";
ajaxOpts.data = {"roles" : checkedRoleItemList, "username": username};
}else if(operationType == "edit"){
ajaxOpts.url = "/api/projects/" + projectId + "/members/" + userId;
ajaxOpts.type = "put";
ajaxOpts.data = {"roles" : checkedRoleItemList};
}
new AjaxUtil({
url: ajaxOpts.url,
data: ajaxOpts.data,
type: ajaxOpts.type,
complete: function(jqXhr, status){
if(jqXhr && jqXhr.status == 200){
$("#btnCancel").trigger("click");
listUser(null);
}
},
errors: {
404: i18n.getMessage("user_id_does_not_exist"),
409: i18n.getMessage("user_id_exists"),
403: i18n.getMessage("insufficient_privileges")
}
}).exec();
});
var name_mapping = {
"projectAdmin": "Project Admin",
"developer": "Developer",
"guest": "Guest"
}
function listUserByProjectCallback(userList){
var loginedUserId = $("#userId").val();
var loginedUserRoleId = $("#roleId").val();
var ownerId = $("#ownerId").val();
$("#tblUser tbody tr").remove();
for(var i = 0; i < userList.length; i++){
var userId = userList[i].user_id;
var roleId = userList[i].role_id;
var username = userList[i].username;
var row = '<tr>' +
'<td>' + username + '</td>' +
'<td>' + name_mapping[userList[i].role_name] + '</td>' +
'<td>';
var isShowOperations = true;
if(loginedUserRoleId >= 3 /*role: developer guest*/){
isShowOperations = false;
}else if(ownerId == userId){
isShowOperations = false;
}else if (loginedUserId == userId){
isShowOperations = false;
}
if(isShowOperations){
row += '<a href="#" userid="' + userId + '" class="glyphicon glyphicon-pencil" data-toggle="modal" data-target="#dlgUser"></a>&nbsp;' +
'<a href="#" userid="' + userId + '" roleid="' + roleId + '" class="glyphicon glyphicon-trash"></a>';
}
row += '</td></tr>';
$("#tblUser tbody").append(row);
}
}
function searchAccessLogCallback(LogList){
$("#tabOperationLog tbody tr").remove();
$.each(LogList || [], function(i, e){
$("#tabOperationLog tbody").append(
'<tr>' +
'<td>' + e.username + '</td>' +
'<td>' + e.repo_name + '</td>' +
'<td>' + e.repo_tag + '</td>' +
'<td>' + e.operation + '</td>' +
'<td>' + moment(new Date(e.op_time)).format("YYYY-MM-DD HH:mm:ss") + '</td>' +
'</tr>');
});
}
function getUserRoleCallback(userId){
new AjaxUtil({
url: "/api/projects/" + $("#projectId").val() + "/members/" + userId,
type: "get",
success: function(data, status, xhr){
var user = data;
$("#operationType").val("edit");
$("#editUserId").val(user.user_id);
$("#spnSearch").hide();
$("#txtUserName").val(user.username);
$("#txtUserName").prop("disabled", true);
$("#btnSave").removeClass("disabled");
$("#dlgUserTitle").text(i18n.getMessage("edit_members"));
$("#lstRole input[name=chooseRole]:radio").not('[value=' + user.role_id + ']').prop("checked", false)
$.each(user.roles, function(i, e){
$("#lstRole input[name=chooseRole]:radio").filter('[value=' + e.role_id + ']').prop("checked", "checked");
});
}
}).exec();
}
function listUser(username){
$.when(
new AjaxUtil({
url: "/api/projects/" + $("#projectId").val() + "/members?username=" + (username == null ? "" : username),
type: "get",
errors: {
403: ""
},
success: function(data, status, xhr){
return data || [];
}
}).exec()
).done(function(userList){
listUserByProjectCallback(userList || []);
$("#tblUser .glyphicon-pencil").on("click", function(e){
var userId = $(this).attr("userid")
getUserRoleCallback(userId);
});
$("#tblUser .glyphicon-trash").on("click", function(){
var userId = $(this).attr("userid");
new AjaxUtil({
url: "/api/projects/" + $("#projectId").val() + "/members/" + userId,
type: "delete",
complete: function(jqXhr, status){
if(jqXhr && jqXhr.status == 200){
listUser(null);
}
}
}).exec();
});
});
}
listUser(null);
listOperationLogs();
function listOperationLogs(){
var projectId = $("#projectId").val();
$.when(
new AjaxUtil({
url : "/api/projects/" + projectId + "/logs/filter",
data: {},
type: "post",
success: function(data){
return data || [];
}
}).exec()
).done(function(operationLogs){
searchAccessLogCallback(operationLogs);
});
}
$("#btnSearchUser").on("click", function(){
var username = $("#txtSearchUser").val();
if($.trim(username).length == 0){
username = null;
}
listUser(username);
});
function toUTCSeconds(date, hour, min, sec) {
var t = new Date(date);
t.setHours(hour);
t.setMinutes(min);
t.setSeconds(sec);
var utcTime = new Date(t.getUTCFullYear(),
t.getUTCMonth(),
t.getUTCDate(),
t.getUTCHours(),
t.getUTCMinutes(),
t.getUTCSeconds());
return utcTime.getTime() / 1000;
}
$("#btnFilterLog").on("click", function(){
var projectId = $("#projectId").val();
var username = $("#txtSearchUserName").val();
var beginTimestamp = 0;
var endTimestamp = 0;
if($("#begindatepicker").val() != ""){
beginTimestamp = toUTCSeconds($("#begindatepicker").val(), 0, 0, 0);
}
if($("#enddatepicker").val() != ""){
endTimestamp = toUTCSeconds($("#enddatepicker").val(), 23, 59, 59);
}
new AjaxUtil({
url: "/api/projects/" + projectId + "/logs/filter",
data:{"username":username, "project_id" : projectId, "keywords" : getKeyWords() , "beginTimestamp" : beginTimestamp, "endTimestamp" : endTimestamp},
type: "post",
success: function(data, status, xhr){
if(xhr && xhr.status == 200){
searchAccessLogCallback(data);
return utcTime.getTime() / 1000;
}
}
}).exec();
});
$("#spnFilterOption input[name=chkAll]").on("click", function(){
$("#spnFilterOption input[name=chkOperation]").prop("checked", $(this).prop("checked"));
});
$("#spnFilterOption input[name=chkOperation]").on("click", function(){
if(!$(this).prop("checked")){
$("#spnFilterOption input[name=chkAll]").prop("checked", false);
}
var selectedAll = true;
$("#spnFilterOption input[name=chkOperation]").each(function(i, e){
if(!$(e).prop("checked")){
selectedAll = false;
}
});
if(selectedAll){
$("#spnFilterOption input[name=chkAll]").prop("checked", true);
}
});
function getKeyWords(){
var keywords = "";
var checkedItemList=$("#spnFilterOption input[name=chkOperation]:checked");
var keywords = [];
$.each(checkedItemList, function(i, e){
var itemValue = $(e).val();
if(itemValue == "others" && $.trim($("#txtOthers").val()).length > 0){
keywords.push($("#txtOthers").val());
}else{
keywords.push($(e).val());
}
});
return keywords.join("/");
}
$('#datetimepicker1').datetimepicker({
locale: i18n.getLocale(),
ignoreReadonly: true,
format: 'L',
showClear: true
});
$('#datetimepicker2').datetimepicker({
locale: i18n.getLocale(),
ignoreReadonly: true,
format: 'L',
showClear: true
});
});
}
$(document).on("keydown", function(e){
if(e.keyCode == 13){
e.preventDefault();
if($("#tabItemDetail li:eq(0)").is(":focus") || $("#txtRepoName").is(":focus")){
$("#btnSearchRepo").trigger("click");
}else if($("#tabItemDetail li:eq(1)").is(":focus") || $("#txtSearchUser").is(":focus")){
$("#btnSearchUser").trigger("click");
}else if($("#tabItemDetail li:eq(2)").is(":focus") || $("#txtSearchUserName").is(":focus")){
$("#btnFilterLog").trigger("click");
}else if($("#txtUserName").is(":focus") || $("#lstRole :radio").is(":focus")){
$("#btnSave").trigger("click");
}
$("#btnFilterLog").on("click", function(){
var projectId = $("#projectId").val();
var username = $("#txtSearchUserName").val();
var beginTimestamp = 0;
var endTimestamp = 0;
if($("#begindatepicker").val() != ""){
beginTimestamp = toUTCSeconds($("#begindatepicker").val(), 0, 0, 0);
}
if($("#enddatepicker").val() != ""){
endTimestamp = toUTCSeconds($("#enddatepicker").val(), 23, 59, 59);
}
new AjaxUtil({
url: "/api/projects/" + projectId + "/logs/filter",
data:{"username":username, "project_id" : Number(projectId), "keywords" : getKeyWords() , "begin_timestamp" : beginTimestamp, "end_timestamp" : endTimestamp},
type: "post",
success: function(data, status, xhr){
if(xhr && xhr.status == 200){
searchAccessLogCallback(data);
}
}
}).exec();
});
$("#spnFilterOption input[name=chkAll]").on("click", function(){
$("#spnFilterOption input[name=chkOperation]").prop("checked", $(this).prop("checked"));
});
$("#spnFilterOption input[name=chkOperation]").on("click", function(){
if(!$(this).prop("checked")){
$("#spnFilterOption input[name=chkAll]").prop("checked", false);
}
var selectedAll = true;
$("#spnFilterOption input[name=chkOperation]").each(function(i, e){
if(!$(e).prop("checked")){
selectedAll = false;
}
});
if(selectedAll){
$("#spnFilterOption input[name=chkAll]").prop("checked", true);
}
});
function getKeyWords(){
var keywords = "";
var checkedItemList=$("#spnFilterOption input[name=chkOperation]:checked");
var keywords = [];
$.each(checkedItemList, function(i, e){
var itemValue = $(e).val();
if(itemValue == "others" && $.trim($("#txtOthers").val()).length > 0){
keywords.push($("#txtOthers").val());
}else{
keywords.push($(e).val());
}
});
return keywords.join("/");
}
$('#datetimepicker1').datetimepicker({
locale: i18n.getLocale(),
ignoreReadonly: true,
format: 'L',
showClear: true
});
$('#datetimepicker2').datetimepicker({
locale: i18n.getLocale(),
ignoreReadonly: true,
format: 'L',
showClear: true
});
});
}
$(document).on("keydown", function(e){
if(e.keyCode == 13){
e.preventDefault();
if($("#tabItemDetail li:eq(0)").is(":focus") || $("#txtRepoName").is(":focus")){
$("#btnSearchRepo").trigger("click");
}else if($("#tabItemDetail li:eq(1)").is(":focus") || $("#txtSearchUser").is(":focus")){
$("#btnSearchUser").trigger("click");
}else if($("#tabItemDetail li:eq(2)").is(":focus") || $("#txtSearchUserName").is(":focus")){
$("#btnFilterLog").trigger("click");
}else if($("#txtUserName").is(":focus") || $("#lstRole :radio").is(":focus")){
$("#btnSave").trigger("click");
}
});
})
}
});
})

View File

@ -24,7 +24,7 @@ jQuery(function(){
},
error: function(jqXhr){
if(jqXhr.status == 401)
return false;
return false;
}
}).exec();

View File

@ -13,13 +13,13 @@
limitations under the License.
*/
jQuery(function(){
new AjaxUtil({
url: "/api/users/current",
type: "get",
success: function(data, status, xhr){
if(xhr && xhr.status == 200){
if(data.HasAdminRole == 1) {
if(data.has_admin_role == 1) {
renderForAdminRole();
}
renderForAnyRole();
@ -29,57 +29,57 @@ jQuery(function(){
function renderForAnyRole(){
$("#tabProject a:first").tab("show");
$(document).on("keydown", function(e){
if(e.keyCode == 13){
e.preventDefault();
if($("#tabProject li:eq(0)").is(":focus") || $("#txtSearchProject").is(":focus")){
$("#btnSearch").trigger("click");
}else if($("#tabProject li:eq(1)").is(":focus") || $("#txtSearchPublicProjects").is(":focus")){
$("#btnSearchPublicProjects").trigger("click");
}else if($("#tabProject li:eq(1)").is(":focus") || $("#txtSearchUsername").is(":focus")){
$("#btnSearchUsername").trigger("click");
}else if($("#dlgAddProject").is(":focus") || $("#projectName").is(":focus")){
$("#btnSave").trigger("click");
}
e.preventDefault();
if($("#tabProject li:eq(0)").is(":focus") || $("#txtSearchProject").is(":focus")){
$("#btnSearch").trigger("click");
}else if($("#tabProject li:eq(1)").is(":focus") || $("#txtSearchPublicProjects").is(":focus")){
$("#btnSearchPublicProjects").trigger("click");
}else if($("#tabProject li:eq(1)").is(":focus") || $("#txtSearchUsername").is(":focus")){
$("#btnSearchUsername").trigger("click");
}else if($("#dlgAddProject").is(":focus") || $("#projectName").is(":focus")){
$("#btnSave").trigger("click");
}
}
});
function listProject(projectName, isPublic){
currentPublic = isPublic;
$.when(
new AjaxUtil({
url: "/api/projects?is_public=" + isPublic + "&project_name=" + (projectName == null ? "" : projectName) + "&timestamp=" + new Date().getTime(),
type: "get",
success: function(data, status, xhr){
$("#tblProject tbody tr").remove();
$.each(data || [], function(i, e){
var row = '<tr>' +
'<td style="vertical-align: middle;"><a href="/registry/detail?project_id=' + e.ProjectId + '">' + e.Name + '</a></td>' +
'<td style="vertical-align: middle;">' + moment(new Date(e.CreationTime)).format("YYYY-MM-DD HH:mm:ss") + '</td>';
if(e.Public == 1 && e.Togglable){
row += '<td><button type="button" class="btn btn-success" projectid="' + e.ProjectId + '">' + i18n.getMessage("button_on")+ '</button></td>'
} else if (e.Public == 1) {
row += '<td><button type="button" class="btn btn-success" projectid="' + e.ProjectId + '" disabled>' + i18n.getMessage("button_on")+ '</button></td>';
} else if (e.Public == 0 && e.Togglable) {
row += '<td><button type="button" class="btn btn-danger" projectid="' + e.ProjectId + '">' + i18n.getMessage("button_off")+ '</button></td>';
} else if (e.Public == 0) {
row += '<td><button type="button" class="btn btn-danger" projectid="' + e.ProjectId + '" disabled>' + i18n.getMessage("button_off")+ '</button></td>';
row += '</tr>';
}
$("#tblProject tbody").append(row);
});
}
}).exec())
url: "/api/projects?is_public=" + isPublic + "&project_name=" + (projectName == null ? "" : projectName) + "&timestamp=" + new Date().getTime(),
type: "get",
success: function(data, status, xhr){
$("#tblProject tbody tr").remove();
$.each(data || [], function(i, e){
var row = '<tr>' +
'<td style="vertical-align: middle;"><a href="/registry/detail?project_id=' + e.project_id + '">' + e.name + '</a></td>' +
'<td style="vertical-align: middle;">' + moment(new Date(e.creation_time)).format("YYYY-MM-DD HH:mm:ss") + '</td>';
if(e.public == 1 && e.Togglable){
row += '<td><button type="button" class="btn btn-success" projectid="' + e.project_id + '">' + i18n.getMessage("button_on")+ '</button></td>'
} else if (e.public == 1) {
row += '<td><button type="button" class="btn btn-success" projectid="' + e.project_id + '" disabled>' + i18n.getMessage("button_on")+ '</button></td>';
} else if (e.public == 0 && e.Togglable) {
row += '<td><button type="button" class="btn btn-danger" projectid="' + e.project_id + '">' + i18n.getMessage("button_off")+ '</button></td>';
} else if (e.public == 0) {
row += '<td><button type="button" class="btn btn-danger" projectid="' + e.project_id + '" disabled>' + i18n.getMessage("button_off")+ '</button></td>';
row += '</tr>';
}
$("#tblProject tbody").append(row);
});
}
}).exec())
.done(function() {
$("#tblProject tbody tr :button").on("click", function(){
var projectId = $(this).attr("projectid");
var self = this;
new AjaxUtil({
url: "/api/projects/" + projectId,
data: {"public": ($(self).hasClass("btn-success") ? false : true)},
type: "put",
complete: function(jqXhr, status) {
$("#tblProject tbody tr :button").on("click", function(){
var projectId = $(this).attr("projectid");
var self = this;
new AjaxUtil({
url: "/api/projects/" + projectId,
data: {"public": ($(self).hasClass("btn-success") ? false : true)},
type: "put",
complete: function(jqXhr, status) {
if($(self).hasClass("btn-success")){
$(self).removeClass("btn-success").addClass("btn-danger");
$(self).html(i18n.getMessage("button_off"));
@ -88,9 +88,9 @@ jQuery(function(){
$(self).html(i18n.getMessage("button_on"));
}
}
}).exec();
});
});
}).exec();
});
});
}
listProject(null, 0);
var currentPublic = 0;
@ -119,7 +119,7 @@ jQuery(function(){
$("#projectName").val("");
$("#projectName").parent().addClass("has-feedback");
$("#projectName").siblings("span").removeClass("glyphicon-warning-sign").removeClass("glyphicon-ok");
$("#isPublic").prop('checked', false);
$("#isPublic").prop('checked', false);
});
$("#btnSave").on("click", function(){
@ -161,52 +161,52 @@ jQuery(function(){
$("#tblUser tbody tr").remove();
$.each(data || [], function(i, e){
var row = '<tr>' +
'<td style="vertical-align: middle;">' + e.username + '</td>' +
'<td style="vertical-align: middle;">' + e.email + '</td>';
if(e.HasAdminRole == 1){
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-success" userid="' + e.UserId + '">' + i18n.getMessage("button_on") + '</button></td>';
'<td style="vertical-align: middle;">' + e.username + '</td>' +
'<td style="vertical-align: middle;">' + e.email + '</td>';
if(e.has_admin_role == 1){
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-success" userid="' + e.user_id + '">' + i18n.getMessage("button_on") + '</button></td>';
} else {
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-danger" userid="' + e.UserId + '">' + i18n.getMessage("button_off") + '</button></td>';
row += '<td style="padding-left: 30px;"><button type="button" class="btn btn-danger" userid="' + e.user_id + '">' + i18n.getMessage("button_off") + '</button></td>';
}
row += '<td style="padding-left: 30px; vertical-align: middle;"><a href="#" style="visibility: hidden;" class="tdDeleteUser" userid="' + e.UserId + '" username="' + e.Username + '"><span class="glyphicon glyphicon-trash"></span></a></td>';
row += '<td style="padding-left: 30px; vertical-align: middle;"><a href="#" style="visibility: hidden;" class="tdDeleteUser" userid="' + e.user_id + '" username="' + e.username + '"><span class="glyphicon glyphicon-trash"></span></a></td>';
row += '</tr>';
$("#tblUser tbody").append(row);
});
}
}).exec()
).done(function(){
$("#tblUser tbody tr :button").on("click",function(){
var userId = $(this).attr("userid");
var self = this;
new AjaxUtil({
url: "/api/users/" + userId,
type: "put",
complete: function(jqXhr, status){
if(jqXhr && jqXhr.status == 200){
if($(self).hasClass("btn-success")){
$(self).removeClass("btn-success").addClass("btn-danger");
$(self).html(i18n.getMessage("button_off"));
}else{
$(self).removeClass("btn-danger").addClass("btn-success");
$(self).html(i18n.getMessage("button_on"));
}
}
}
}).exec();
});
$("#tblUser tbody tr").on("mouseover", function(){
$(".tdDeleteUser", this).css({"visibility":"visible"});
}).on("mouseout", function(){
$(".tdDeleteUser", this).css({"visibility":"hidden"});
});
$("#tblUser tbody tr .tdDeleteUser").on("click", function(){
var userId = $(this).attr("userid");
$("#dlgModal")
).done(function(){
$("#tblUser tbody tr :button").on("click",function(){
var userId = $(this).attr("userid");
var self = this;
new AjaxUtil({
url: "/api/users/" + userId,
type: "put",
complete: function(jqXhr, status){
if(jqXhr && jqXhr.status == 200){
if($(self).hasClass("btn-success")){
$(self).removeClass("btn-success").addClass("btn-danger");
$(self).html(i18n.getMessage("button_off"));
}else{
$(self).removeClass("btn-danger").addClass("btn-success");
$(self).html(i18n.getMessage("button_on"));
}
}
}
}).exec();
});
$("#tblUser tbody tr").on("mouseover", function(e){
$(".tdDeleteUser", this).css({"visibility":"visible"});
}).on("mouseout", function(e){
$(".tdDeleteUser", this).css({"visibility":"hidden"});
});
$("#tblUser tbody tr .tdDeleteUser").on("click", function(e){
var userId = $(this).attr("userid");
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("delete_user"),
"content": i18n.getMessage("are_you_sure_to_delete_user") + $(this).attr("username") + " ?",
"enableCancel": true,
"callback": function(){
"callback": function(){
new AjaxUtil({
url: "/api/users/" + userId,
type: "delete",
@ -218,17 +218,18 @@ jQuery(function(){
error: function(jqXhr){}
}).exec();
}
});
});
});
}
listUserAdminRole(null);
$("#btnSearchUsername").on("click", function(){
var username = $("#txtSearchUsername").val();
if($.trim(username).length == 0){
username = null;
}
listUserAdminRole(username);
});
}
listUserAdminRole(null);
$("#btnSearchUsername").on("click", function(){
var username = $("#txtSearchUsername").val();
if($.trim(username).length == 0){
username = null;
}
listUserAdminRole(username);
});
}
})
})

View File

@ -30,14 +30,14 @@ jQuery(function(){
$("#btnPageSignUp").on("click", function(){
validateOptions.Validate(function() {
var username = $.trim($("#Username").val());
var email = $.trim($("#Email").val());
var password = $.trim($("#Password").val());
var confirmedPassword = $.trim($("#ConfirmedPassword").val());
var realname = $.trim($("#Realname").val());
var comment = $.trim($("#Comment").val());
var isAdmin = $("#isAdmin").val();
var username = $.trim($("#Username").val());
var email = $.trim($("#Email").val());
var password = $.trim($("#Password").val());
var confirmedPassword = $.trim($("#ConfirmedPassword").val());
var realname = $.trim($("#Realname").val());
var comment = $.trim($("#Comment").val());
var isAdmin = $("#isAdmin").val();
new AjaxUtil({
url : "/api/users",
data: {"username": username, "password": password, "realname": realname, "comment": comment, "email": email},
@ -47,29 +47,29 @@ jQuery(function(){
},
error:function(jqxhr, status, error){
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("title_sign_up"),
"content": i18n.getMessage("internal_error"),
"callback": function(){
return;
}
});
.dialogModal({
"title": i18n.getMessage("title_sign_up"),
"content": i18n.getMessage("internal_error"),
"callback": function(){
return;
}
});
},
complete: function(xhr, status){
$("#btnPageSignUp").prop("disabled", false);
if(xhr && xhr.status == 201){
$("#dlgModal")
.dialogModal({
"title": isAdmin == "true" ? i18n.getMessage("title_add_user") : i18n.getMessage("title_sign_up"),
"content": isAdmin == "true" ? i18n.getMessage("added_user_successfully") : i18n.getMessage("registered_successfully"),
"callback": function(){
if(isAdmin == "true") {
document.location = "/registry/project";
}else{
document.location = "/signIn";
}
.dialogModal({
"title": isAdmin == "true" ? i18n.getMessage("title_add_user") : i18n.getMessage("title_sign_up"),
"content": isAdmin == "true" ? i18n.getMessage("added_user_successfully") : i18n.getMessage("registered_successfully"),
"callback": function(){
if(isAdmin == "true") {
document.location = "/registry/project";
}else{
document.location = "/signIn";
}
});
}
});
}
}
}).exec();

View File

@ -17,11 +17,11 @@ jQuery(function(){
$("#Password,#ConfirmedPassword").on("blur", validateCallback);
validateOptions.Items = ["#Password", "#ConfirmedPassword"];
function bindEnterKey(){
function bindEnterKey(){
$(document).on("keydown", function(e){
if(e.keyCode == 13){
e.preventDefault();
$("#btnSubmit").trigger("click");
e.preventDefault();
$("#btnSubmit").trigger("click");
}
});
}
@ -30,7 +30,6 @@ jQuery(function(){
}
bindEnterKey();
var spinner = new Spinner({scale:1}).spin();
$("#btnSubmit").on("click", function(){
@ -42,20 +41,20 @@ jQuery(function(){
"type": "post",
"data": {"reset_uuid": resetUuid, "password": password},
"beforeSend": function(e){
unbindEnterKey();
$("h1").append(spinner.el);
$("#btnSubmit").prop("disabled", true);
unbindEnterKey();
$("h1").append(spinner.el);
$("#btnSubmit").prop("disabled", true);
},
"success": function(data, status, xhr){
if(xhr && xhr.status == 200){
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("title_reset_password"),
"content": i18n.getMessage("reset_password_successfully"),
"callback": function(){
document.location="/signIn";
}
});
.dialogModal({
"title": i18n.getMessage("title_reset_password"),
"content": i18n.getMessage("reset_password_successfully"),
"callback": function(){
document.location="/signIn";
}
});
}
},
@ -66,14 +65,14 @@ jQuery(function(){
"error": function(jqXhr, status, error){
if(jqXhr){
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("title_reset_password"),
"content": i18n.getMessage(jqXhr.responseText),
"callback": function(){
bindEnterKey();
return;
}
});
.dialogModal({
"title": i18n.getMessage("title_reset_password"),
"content": i18n.getMessage(jqXhr.responseText),
"callback": function(){
bindEnterKey();
return;
}
});
}
}
});

View File

@ -60,12 +60,12 @@ jQuery(function(){
$.each(data, function(i, e){
var project, description, repoName;
switch(discriminator){
case "project":
case "project":
project = new Project(e.id, e.name, e.public);
description = project.name;
repoName = "";
break;
case "repository":
case "repository":
project = new Project(e.project_id, e.project_name, e.project_public);
description = e.repository_name;
repoName = e.repository_name.substring(e.repository_name.lastIndexOf("/") + 1);

View File

@ -56,7 +56,7 @@ jQuery(function(){
success: function(jqXhr, status){
var lastUri = location.search;
if(lastUri != "" && lastUri.indexOf("=") > 0){
document.location = decodeURIComponent(lastUri.split("=")[1]);
document.location = decodeURIComponent(lastUri.split("=")[1]);
}else{
document.location = "/registry/project";
}
@ -69,10 +69,10 @@ jQuery(function(){
i18nKey = "check_your_username_or_password"
}
$("#dlgModal")
.dialogModal({
"title": i18n.getMessage("title_login_failed"),
"content": i18n.getMessage(i18nKey)
});
.dialogModal({
"title": i18n.getMessage("title_login_failed"),
"content": i18n.getMessage(i18nKey)
});
}
});
});

View File

@ -26,18 +26,18 @@ var validateOptions = {
"Username" :{
"Required": { "value" : true, "errMsg" : i18n.getMessage("username_is_required")},
"CheckExist": { "value" : function(value){
var result = true;
$.ajax({
url: "/userExists",
data: {"target": "username", "value" : value},
var result = true;
$.ajax({
url: "/userExists",
data: {"target": "username", "value" : value},
dataType: "json",
type: "post",
async: false,
success: function(data){
result = data;
}
});
return result;
type: "post",
async: false,
success: function(data){
result = data;
}
});
return result;
}, "errMsg" : i18n.getMessage("username_has_been_taken")},
"MaxLength": {"value" : 20, "errMsg" : i18n.getMessage("username_is_too_long")},
"IllegalChar": {"value": [",","~","#", "$", "%"] , "errMsg": i18n.getMessage("username_contains_illegal_chars")}
@ -45,40 +45,40 @@ var validateOptions = {
"Email" :{
"Required": { "value" : true, "errMsg" : i18n.getMessage("email_is_required")},
"RegExp": {"value": /^(([^<>()[\]\\.,;:\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,}))$/,
"errMsg": i18n.getMessage("email_contains_illegal_chars")},
"errMsg": i18n.getMessage("email_contains_illegal_chars")},
"CheckExist": { "value" : function(value){
var result = true;
$.ajax({
url: "/userExists",
data: {"target": "email", "value": value},
dataType: "json",
type: "post",
async: false,
success: function(data){
result = data;
}
});
return result;
}, "errMsg" : i18n.getMessage("email_has_been_taken")}
var result = true;
$.ajax({
url: "/userExists",
data: {"target": "email", "value": value},
dataType: "json",
type: "post",
async: false,
success: function(data){
result = data;
}
});
return result;
}, "errMsg" : i18n.getMessage("email_has_been_taken")}
},
"EmailF" :{
"Required": { "value" : true, "errMsg" : i18n.getMessage("email_is_required")},
"RegExp": {"value": /^(([^<>()[\]\\.,;:\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,}))$/,
"errMsg": i18n.getMessage("email_content_illegal")},
"errMsg": i18n.getMessage("email_content_illegal")},
"CheckIfNotExist": { "value" : function(value){
var result = true;
$.ajax({
url: "/userExists",
data: {"target": "email", "value": value},
dataType: "json",
type: "post",
async: false,
success: function(data){
result = data;
}
});
return result;
}, "errMsg" : i18n.getMessage("email_does_not_exist")}
var result = true;
$.ajax({
url: "/userExists",
data: {"target": "email", "value": value},
dataType: "json",
type: "post",
async: false,
success: function(data){
result = data;
}
});
return result;
}, "errMsg" : i18n.getMessage("email_does_not_exist")}
},
"Realname" :{
"Required": { "value" : true, "errMsg" : i18n.getMessage("realname_is_required")},
@ -119,7 +119,7 @@ function validateCallback(target){
var currentId = $(target).attr("id");
var validateItem = validateOptions[currentId];
var errMsg = "";
var errMsg = "";
for(var checkTitle in validateItem){

View File

@ -0,0 +1,23 @@
/*
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.
*/
package HarborAPI
type AccessLog struct {
Username string `json:"username,omitempty"`
Keywords string `json:"keywords,omitempty"`
BeginTimestamp int32 `json:"beginTimestamp,omitempty"`
EndTimestamp int32 `json:"endTimestamp,omitempty"`
}

View File

@ -0,0 +1,224 @@
//Package HarborAPI
//These APIs provide services for manipulating Harbor project.
package HarborAPI
import (
"encoding/json"
//"fmt"
"io/ioutil"
"net/http"
"github.com/dghubble/sling"
)
type HarborAPI struct {
basePath string
}
func NewHarborAPI() *HarborAPI {
return &HarborAPI{
basePath: "http://localhost",
}
}
func NewHarborAPIWithBasePath(basePath string) *HarborAPI {
return &HarborAPI{
basePath: basePath,
}
}
type UsrInfo struct {
Name string
Passwd string
}
//Search for projects and repositories
//Implementation Notes
//The Search endpoint returns information about the projects and repositories
//offered at public status or related to the current logged in user.
//The response includes the project and repository list in a proper display order.
//@param q Search parameter for project and repository name.
//@return []Search
//func (a HarborAPI) SearchGet (q string) (Search, error) {
func (a HarborAPI) SearchGet(q string) (Search, error) {
_sling := sling.New().Get(a.basePath)
// create path and map variables
path := "/api/search"
_sling = _sling.Path(path)
type QueryParams struct {
Query string `url:"q,omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{Query: q})
// accept header
accepts := []string{"application/json", "text/plain"}
for key := range accepts {
_sling = _sling.Set("Accept", accepts[key])
break // only use the first Accept
}
req, err := _sling.Request()
client := &http.Client{}
httpResponse, err := client.Do(req)
defer httpResponse.Body.Close()
body, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
// handle error
}
var successPayload = new(Search)
err = json.Unmarshal(body, &successPayload)
return *successPayload, err
}
//Create a new project.
//Implementation Notes
//This endpoint is for user to create a new project.
//@param project New created project.
//@return void
//func (a HarborAPI) ProjectsPost (prjUsr UsrInfo, project Project) (int, error) {
func (a HarborAPI) ProjectsPost(prjUsr UsrInfo, project Project) (int, error) {
_sling := sling.New().Post(a.basePath)
// create path and map variables
path := "/api/projects"
_sling = _sling.Path(path)
// accept header
accepts := []string{"application/json", "text/plain"}
for key := range accepts {
_sling = _sling.Set("Accept", accepts[key])
break // only use the first Accept
}
// body params
_sling = _sling.BodyJSON(project)
req, err := _sling.Request()
req.SetBasicAuth(prjUsr.Name, prjUsr.Passwd)
client := &http.Client{}
httpResponse, err := client.Do(req)
defer httpResponse.Body.Close()
return httpResponse.StatusCode, err
}
//Delete a repository or a tag in a repository.
//Delete a repository or a tag in a repository.
//This endpoint let user delete repositories and tags with repo name and tag.\n
//@param repoName The name of repository which will be deleted.
//@param tag Tag of a repository.
//@return void
//func (a HarborAPI) RepositoriesDelete(prjUsr UsrInfo, repoName string, tag string) (int, error) {
func (a HarborAPI) RepositoriesDelete(prjUsr UsrInfo, repoName string, tag string) (int, error) {
_sling := sling.New().Delete(a.basePath)
// create path and map variables
path := "/api/repositories"
_sling = _sling.Path(path)
type QueryParams struct {
RepoName string `url:"repo_name,omitempty"`
Tag string `url:"tag,omitempty"`
}
_sling = _sling.QueryStruct(&QueryParams{RepoName: repoName, Tag: tag})
// accept header
accepts := []string{"application/json", "text/plain"}
for key := range accepts {
_sling = _sling.Set("Accept", accepts[key])
break // only use the first Accept
}
req, err := _sling.Request()
req.SetBasicAuth(prjUsr.Name, prjUsr.Passwd)
//fmt.Printf("request %+v", req)
client := &http.Client{}
httpResponse, err := client.Do(req)
defer httpResponse.Body.Close()
if err != nil {
// handle error
}
return httpResponse.StatusCode, err
}
//Return projects created by Harbor
//func (a HarborApi) ProjectsGet (projectName string, isPublic int32) ([]Project, error) {
// }
//Check if the project name user provided already exists.
//func (a HarborApi) ProjectsHead (projectName string) (error) {
//}
//Get access logs accompany with a relevant project.
//func (a HarborApi) ProjectsProjectIdLogsFilterPost (projectId int32, accessLog AccessLog) ([]AccessLog, error) {
//}
//Return a project&#39;s relevant role members.
//func (a HarborApi) ProjectsProjectIdMembersGet (projectId int32) ([]Role, error) {
//}
//Add project role member accompany with relevant project and user.
//func (a HarborApi) ProjectsProjectIdMembersPost (projectId int32, roles RoleParam) (error) {
//}
//Delete project role members accompany with relevant project and user.
//func (a HarborApi) ProjectsProjectIdMembersUserIdDelete (projectId int32, userId int32) (error) {
//}
//Return role members accompany with relevant project and user.
//func (a HarborApi) ProjectsProjectIdMembersUserIdGet (projectId int32, userId int32) ([]Role, error) {
//}
//Update project role members accompany with relevant project and user.
//func (a HarborApi) ProjectsProjectIdMembersUserIdPut (projectId int32, userId int32, roles RoleParam) (error) {
//}
//Update properties for a selected project.
//func (a HarborApi) ProjectsProjectIdPut (projectId int32, project Project) (error) {
//}
//Get repositories accompany with relevant project and repo name.
//func (a HarborApi) RepositoriesGet (projectId int32, q string) ([]Repository, error) {
//}
//Get manifests of a relevant repository.
//func (a HarborApi) RepositoriesManifestGet (repoName string, tag string) (error) {
//}
//Get tags of a relevant repository.
//func (a HarborApi) RepositoriesTagsGet (repoName string) (error) {
//}
//Get registered users of Harbor.
//func (a HarborApi) UsersGet (userName string) ([]User, error) {
//}
//Creates a new user account.
//func (a HarborApi) UsersPost (user User) (error) {
//}
//Mark a registered user as be removed.
//func (a HarborApi) UsersUserIdDelete (userId int32) (error) {
//}
//Change the password on a user that already exists.
//func (a HarborApi) UsersUserIdPasswordPut (userId int32, password Password) (error) {
//}
//Update a registered user to change to be an administrator of Harbor.
//func (a HarborApi) UsersUserIdPut (userId int32) (error) {
//}

View File

@ -0,0 +1,15 @@
// HarborLogout.go
package HarborAPI
import (
"net/http"
)
func (a HarborAPI) HarborLogout() (int, error) {
response, err := http.Get(a.basePath + "/logout")
defer response.Body.Close()
return response.StatusCode, err
}

View File

@ -0,0 +1,28 @@
// HarborLogon.go
package HarborAPI
import (
"io/ioutil"
"net/http"
"net/url"
"strings"
)
func (a HarborAPI) HarborLogin(user UsrInfo) (int, error) {
v := url.Values{}
v.Set("principal", user.Name)
v.Set("password", user.Passwd)
body := ioutil.NopCloser(strings.NewReader(v.Encode())) //endode v:[body struce]
client := &http.Client{}
reqest, err := http.NewRequest("POST", a.basePath+"/login", body)
reqest.Header.Set("Content-Type", "application/x-www-form-urlencoded;param=value") //setting post head
resp, err := client.Do(reqest)
defer resp.Body.Close() //close resp.Body
return resp.StatusCode, err
}

View File

@ -0,0 +1,15 @@
package HarborAPI
import ()
type Project struct {
ProjectId int32 `json:"id,omitempty"`
OwnerId int32 `json:"owner_id,omitempty"`
ProjectName string `json:"project_name,omitempty"`
CreationTime string `json:"creation_time,omitempty"`
Deleted int32 `json:"deleted,omitempty"`
UserId int32 `json:"user_id,omitempty"`
OwnerName string `json:"owner_name,omitempty"`
Public bool `json:"public,omitempty"`
Togglable bool `json:"togglable,omitempty"`
}

View File

@ -0,0 +1,9 @@
package HarborAPI
import ()
type Project4Search struct {
ProjectId int32 `json:"id,omitempty"`
ProjectName string `json:"name,omitempty"`
Public int32 `json:"public,omitempty"`
}

View File

@ -0,0 +1,16 @@
package HarborAPI
import (
"time"
)
type Repository struct {
Id string `json:"id,omitempty"`
Parent string `json:"parent,omitempty"`
Created time.Time `json:"created,omitempty"`
DurationDays string `json:"duration_days,omitempty"`
Author string `json:"author,omitempty"`
Architecture string `json:"architecture,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
Os string `json:"os,omitempty"`
}

View File

@ -0,0 +1,9 @@
package HarborAPI
type Repository4Search struct {
ProjectId int32 `json:"project_id,omitempty"`
ProjectName string `json:"project_name,omitempty"`
ProjectPublic int32 `json:"project_public,omitempty"`
RepoName string `json:"repository_name,omitempty"`
}

View File

@ -0,0 +1,7 @@
package HarborAPI
type Role struct {
RoleId int32 `json:"role_id,omitempty"`
RoleCode string `json:"role_code,omitempty"`
RoleName string `json:"role_name,omitempty"`
}

View File

@ -0,0 +1,6 @@
package HarborAPI
type RoleParam struct {
Roles []int32 `json:"roles,omitempty"`
UserName string `json:"user_name,omitempty"`
}

View File

@ -0,0 +1,8 @@
package HarborAPI
import ()
type Search struct {
Projects []Project4Search `json:"project,omitempty"`
Repositories []Repository4Search `json:"repository,omitempty"`
}

View File

@ -0,0 +1,11 @@
package HarborAPI
type User struct {
UserId int32 `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
Email string `json:"email,omitempty"`
Password string `json:"password,omitempty"`
Realname string `json:"realname,omitempty"`
Comment string `json:"comment,omitempty"`
Deleted int32 `json:"deleted,omitempty"`
}

View File

@ -0,0 +1,95 @@
package HarborAPItest
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestAddProject(t *testing.T) {
fmt.Println("Test for Project Add (ProjectsPost) API\n")
assert := assert.New(t)
apiTest := HarborAPI.NewHarborAPI()
//prepare for test
adminEr := &HarborAPI.UsrInfo{"admin", "Harbor1234"}
admin := &HarborAPI.UsrInfo{"admin", "Harbor12345"}
prjUsr := &HarborAPI.UsrInfo{"unknown", "unknown"}
var project HarborAPI.Project
project.ProjectName = "testproject"
project.Public = true
//case 1: admin login fail, expect project creation fail.
fmt.Println("case 1: admin login fail, expect project creation fail.")
resault, err := apiTest.HarborLogin(*adminEr)
if err != nil {
t.Error("Error while admin login", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(401), "Admin login status should be 401")
//t.Log(resault)
}
resault, err = apiTest.ProjectsPost(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(401), "Case 1: Project creation status should be 401")
//t.Log(resault)
}
//case 2: admin successful login, expect project creation success.
fmt.Println("case 2: admin successful login, expect project creation success.")
resault, err = apiTest.HarborLogin(*admin)
if err != nil {
t.Error("Error while admin login", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(200), "Admin login status should be 200")
//t.Log(resault)
}
if resault != 200 {
t.Log(resault)
} else {
prjUsr = admin
}
resault, err = apiTest.ProjectsPost(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(201), "Case 2: Project creation status should be 201")
//t.Log(resault)
}
//case 3: duplicate project name, create project fail
fmt.Println("case 3: duplicate project name, create project fail")
resault, err = apiTest.ProjectsPost(*prjUsr, project)
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(409), "Case 3: Project creation status should be 409")
//t.Log(resault)
}
//resault1, err := apiTest.HarborLogout()
//if err != nil {
// t.Error("Error while admin logout", err.Error())
// t.Log(err)
//} else {
// assert.Equal(resault1, int(200), "Admin logout status")
// //t.Log(resault)
//}
//if resault1 != 200 {
// t.Log(resault)
//}
}

View File

@ -0,0 +1,130 @@
package HarborAPItest
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestRepositoryDelete(t *testing.T) {
fmt.Println("Test for Project Delete (ProjectDelete) API\n")
assert := assert.New(t)
//prepare for test
adminEr := &HarborAPI.UsrInfo{"admin", "Harbor1234"}
admin := &HarborAPI.UsrInfo{"admin", "Harbor12345"}
prjUsr := &HarborAPI.UsrInfo{"unknown", "unknown"}
fmt.Println("Checking repository status...\n")
apiTest := HarborAPI.NewHarborAPI()
var searchResault HarborAPI.Search
searchResault, err := apiTest.SearchGet("library")
//fmt.Printf("%+v\n", resault)
if err != nil {
t.Error("Error while search project or repository", err.Error())
t.Log(err)
} else {
//assert.Equal(searchResault.Repositories[0].RepoName, "library/docker", "1st repo name should be")
if !assert.Equal(searchResault.Repositories[0].RepoName, "library/docker", "1st repo name should be") {
t.Error("fail to find repo 'library/docker'", err.Error())
t.Log(err)
} else {
fmt.Println("repo 'library/docker' exit\n")
}
//assert.Equal(searchResault.Repositories[1].RepoName, "library/hello-world", "2nd repo name should be")
if !assert.Equal(searchResault.Repositories[1].RepoName, "library/hello-world", "2nd repo name should be") {
t.Error("fail to find repo 'library/hello-world'", err.Error())
t.Log(err)
} else {
fmt.Println("repo 'library/hello-world' exit\n")
}
//t.Log(resault)
}
//case 1: admin login fail, expect repo delete fail.
fmt.Println("case 1: admin login fail, expect repo delete fail.")
resault, err := apiTest.HarborLogin(*adminEr)
if err != nil {
t.Error("Error while admin login", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(401), "Admin login status should be 401")
//t.Log(resault)
}
if resault != 401 {
t.Log(resault)
} else {
prjUsr = adminEr
}
resault, err = apiTest.RepositoriesDelete(*prjUsr, "library/docker", "")
if err != nil {
t.Error("Error while delete repository", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(401), "Case 1: Repository delete status should be 401")
//t.Log(resault)
}
//case 2: admin successful login, expect repository delete success.
fmt.Println("case 2: admin successful login, expect repository delete success.")
resault, err = apiTest.HarborLogin(*admin)
if err != nil {
t.Error("Error while admin login", err.Error())
t.Log(err)
} else {
assert.Equal(resault, int(200), "Admin login status should be 200")
//t.Log(resault)
}
if resault != 200 {
t.Log(resault)
} else {
prjUsr = admin
}
resault, err = apiTest.RepositoriesDelete(*prjUsr, "library/docker", "")
if err != nil {
t.Error("Error while delete repository", err.Error())
t.Log(err)
} else {
if assert.Equal(resault, int(200), "Case 2: Repository delete status should be 200") {
fmt.Println("Repository 'library/docker' delete success.")
}
//t.Log(resault)
}
resault, err = apiTest.RepositoriesDelete(*prjUsr, "library/hello-world", "")
if err != nil {
t.Error("Error while delete repository", err.Error())
t.Log(err)
} else {
if assert.Equal(resault, int(200), "Case 2: Repository delete status should be 200") {
fmt.Println("Repository 'hello-world' delete success.")
}
//t.Log(resault)
}
//case 3: delete one repo not exit, expect repo delete fail.
fmt.Println("case 3: delete one repo not exit, expect repo delete fail.")
resault, err = apiTest.RepositoriesDelete(*prjUsr, "library/hello-world", "")
if err != nil {
t.Error("Error while delete repository", err.Error())
t.Log(err)
} else {
if assert.Equal(resault, int(404), "Case 3: Repository delete status should be 404") {
fmt.Println("Repository 'hello-world' not exit.")
}
//t.Log(resault)
}
//if resault.Response.StatusCode != 200 {
// t.Log(resault.Response)
//}
}

View File

@ -0,0 +1,31 @@
package HarborAPItest
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
"github.com/vmware/harbor/tests/apitests/apilib"
)
func TestSearch(t *testing.T) {
fmt.Println("Test for Search (SearchGet) API\n")
assert := assert.New(t)
apiTest := HarborAPI.NewHarborAPI()
var resault HarborAPI.Search
resault, err := apiTest.SearchGet("library")
//fmt.Printf("%+v\n", resault)
if err != nil {
t.Error("Error while search project or repository", err.Error())
t.Log(err)
} else {
assert.Equal(resault.Projects[0].ProjectId, int32(1), "Project id should be equal")
assert.Equal(resault.Projects[0].ProjectName, "library", "Project name should be library")
assert.Equal(resault.Projects[0].Public, int32(1), "Project public status should be 1 (true)")
//t.Log(resault)
}
//if resault.Response.StatusCode != 200 {
// t.Log(resault.Response)
//}
}

4
tests/hostcfg.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
IP=`ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'`
#echo $IP
sudo sed "s/reg.mydomain.org/$IP/" -i Deploy/harbor.cfg

36
tests/startuptest.go Normal file
View File

@ -0,0 +1,36 @@
// Fetch prints the content found at a URL.
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
)
func main() {
time.Sleep(60*time.Second)
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
// fmt.Printf("%s", b)
if strings.Contains(string(b), "Harbor") {
fmt.Printf("sucess!\n")
} else {
os.Exit(1)
}
}
}

10
tests/testprepare.sh Executable file
View File

@ -0,0 +1,10 @@
docker pull hello-world
docker pull docker
docker login -u admin -p Harbor12345 127.0.0.1
docker tag hello-world 127.0.0.1/library/hello-world
docker push 127.0.0.1/library/hello-world
docker tag docker 127.0.0.1/library/docker
docker push 127.0.0.1/library/docker

55
tests/userlogintest.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"flag"
)
func main() {
usrNamePtr := flag.String("name","anaymous","user name")
usrPasswdPtr := flag.String("passwd","anaymous","user password")
flag.Parse()
v := url.Values{}
v.Set("principal", *usrNamePtr)
v.Set("password", *usrPasswdPtr)
body := ioutil.NopCloser(strings.NewReader(v.Encode())) //endode v:[body struce]
fmt.Println(v)
client := &http.Client{}
reqest, err := http.NewRequest("POST", "http://localhost/login", body)
if err != nil {
fmt.Println("Fatal error ", err.Error())
}
reqest.Header.Set("Content-Type", "application/x-www-form-urlencoded;param=value") //setting post head
resp, err := client.Do(reqest)
defer resp.Body.Close() //close resp.Body
fmt.Println("login status: ", resp.StatusCode) //print status code
//content_post, err := ioutil.ReadAll(resp.Body)
//if err != nil {
// fmt.Println("Fatal error ", err.Error())
//}
//fmt.Println(string(content_post)) //print reply
response, err := http.Get("http://localhost/api/logout")
if err != nil {
fmt.Println("Fatal error ", err.Error())
}
defer response.Body.Close()
fmt.Println("logout status: ", resp.StatusCode) //print status code
//content_get, err := ioutil.ReadAll(response.Body)
//fmt.Println(string(content_get))
}

View File

@ -55,6 +55,7 @@ func initRouters() {
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List")
beego.Router("/api/projects/?:id", &api.ProjectAPI{})
beego.Router("/api/projects/:id/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/statistics", &api.StatisticAPI{})
beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog")
beego.Router("/api/users/?:id", &api.UserAPI{})
@ -64,10 +65,17 @@ func initRouters() {
beego.Router("/api/repositories/manifests", &api.RepositoryAPI{}, "get:GetManifests")
beego.Router("/api/jobs/replication/?:id([0-9]+)", &api.RepJobAPI{})
beego.Router("/api/jobs/replication/:id([0-9]+)/log", &api.RepJobAPI{}, "get:GetLog")
beego.Router("/api/policies/replication", &api.RepPolicyAPI{})
beego.Router("/api/policies/replication/:id([0-9]+)", &api.RepPolicyAPI{})
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "get:List")
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
beego.Router("/api/policies/replication/:id([0-9]+)/enablement", &api.RepPolicyAPI{}, "put:UpdateEnablement")
beego.Router("/api/targets/?:id([0-9]+)", &api.TargetAPI{})
beego.Router("/api/targets/", &api.TargetAPI{}, "get:List")
beego.Router("/api/targets/", &api.TargetAPI{}, "post:Post")
beego.Router("/api/targets/:id([0-9]+)", &api.TargetAPI{})
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping")
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
beego.Router("api/logs", &api.LogAPI{})
//external service that hosted on harbor process:
beego.Router("/service/notifications", &service.NotificationHandler{})

View File

@ -16,32 +16,32 @@
<div class="col-sm-4"></div>
<div class="col-sm-4">
<div class="page-header">
<h1>{{i18n .Lang "title_change_password"}}</h1>
<h1>{{i18n .Lang "title_change_password"}}</h1>
</div>
<form class="form">
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="OldPassword" class="control-label">{{i18n .Lang "old_password"}}</label>
<input type="password" class="form-control" id="OldPassword">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
</div>
<div class="form-group has-feedback">
<label for="Password" class="control-label">{{i18n .Lang "new_password"}}</label>
<input type="password" class="form-control" id="Password">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="ConfirmedPassword" class="control-label">{{i18n .Lang "confirm_password"}}</label>
<input type="password" class="form-control" id="ConfirmedPassword">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<div class="text-center">
<button type="button" class="btn btn-default" id="btnSubmit">{{i18n .Lang "button_submit"}}</button>
</div>
</div>
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="OldPassword" class="control-label">{{i18n .Lang "old_password"}}</label>
<input type="password" class="form-control" id="OldPassword">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
</div>
<div class="form-group has-feedback">
<label for="Password" class="control-label">{{i18n .Lang "new_password"}}</label>
<input type="password" class="form-control" id="Password">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="ConfirmedPassword" class="control-label">{{i18n .Lang "confirm_password"}}</label>
<input type="password" class="form-control" id="ConfirmedPassword">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<div class="text-center">
<button type="button" class="btn btn-default" id="btnSubmit">{{i18n .Lang "button_submit"}}</button>
</div>
</div>
</form>
</div>
<div class="col-sm-4"></div>

View File

@ -19,19 +19,19 @@
<h1>{{i18n .Lang "title_forgot_password"}}</h1>
</div>
<form class="form">
<div id="waiting1" class="waiting-nonfluid"></div>
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="EmailF" class="control-label">{{i18n .Lang "email"}}</label>
<input type="email" class="form-control" id="EmailF">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "forgot_password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<div class="text-center">
<button type="button" class="btn btn-default" id="btnSubmit">{{i18n .Lang "button_submit"}}</button>
</div>
</div>
<div id="waiting1" class="waiting-nonfluid"></div>
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="EmailF" class="control-label">{{i18n .Lang "email"}}</label>
<input type="email" class="form-control" id="EmailF">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "forgot_password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<div class="text-center">
<button type="button" class="btn btn-default" id="btnSubmit">{{i18n .Lang "button_submit"}}</button>
</div>
</div>
</form>
</div>
<div class="col-sm-4"></div>

View File

@ -13,25 +13,25 @@
limitations under the License.
-->
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<img class="pull-left" src="static/resources/image/Harbor_Logo_rec.png" alt="Harbor's Logo" height="180" width="360"/>
<p class="pull-left" style="margin-top: 85px; color: #245580; font-size: 30pt; text-align: center; width: 60%;">{{i18n .Lang "index_title"}}</p>
</div>
</div>
<div class="jumbotron">
<div class="container">
<img class="pull-left" src="static/resources/image/Harbor_Logo_rec.png" alt="Harbor's Logo" height="180" width="360"/>
<p class="pull-left" style="margin-top: 85px; color: #245580; font-size: 30pt; text-align: center; width: 60%;">{{i18n .Lang "index_title"}}</p>
</div>
</div>
<div class="container">
<!-- Example row of columns -->
<div class="row">
<div class="col-md-12">
<p>{{i18n .Lang "index_desc"}}</p>
<p>{{i18n .Lang "index_desc_0"}}</p>
<p>{{i18n .Lang "index_desc_1"}}</p>
<p>{{i18n .Lang "index_desc_2"}}</p>
<p>{{i18n .Lang "index_desc_3"}}</p>
<p>{{i18n .Lang "index_desc_4"}}</p>
<p>{{i18n .Lang "index_desc_5"}}</p>
</div>
</div>
</div> <!-- /container -->
<div class="container">
<!-- Example row of columns -->
<div class="row">
<div class="col-md-12">
<p>{{i18n .Lang "index_desc"}}</p>
<p>{{i18n .Lang "index_desc_0"}}</p>
<p>{{i18n .Lang "index_desc_1"}}</p>
<p>{{i18n .Lang "index_desc_2"}}</p>
<p>{{i18n .Lang "index_desc_3"}}</p>
<p>{{i18n .Lang "index_desc_4"}}</p>
<p>{{i18n .Lang "index_desc_5"}}</p>
</div>
</div>
</div> <!-- /container -->
<script src="static/resources/js/login.js"></script>

View File

@ -18,7 +18,7 @@
<li>{{.ProjectName}}</li>
</ol>
<div class="page-header" style="margin-top: -10px;">
<h2>{{.ProjectName}} </h2></h4>{{i18n .Lang "owner"}}: {{.OwnerName}}</h4>
<h2>{{.ProjectName}} </h2><h4>{{i18n .Lang "owner"}}: {{.OwnerName}}</h4>
</div>
<div row="tabpanel">
<div class="row">
@ -29,152 +29,150 @@
<li role="presentation" style="visibility: hidden;"><a href="#tabOperationLog" aria-controls="tabOperationLog" role="tab" data-toggle="tab">{{i18n .Lang "logs"}}</a></li>
</ul>
</div>
<div class="col-md-10">
<input type="hidden" id="projectId" value="{{.ProjectId}}">
<input type="hidden" id="projectName" value="{{.ProjectName}}">
<input type="hidden" id="userId" value="{{.UserId}}">
<input type="hidden" id="ownerId" value="{{.OwnerId}}">
<input type="hidden" id="roleId" value="{{.RoleId}}">
<input type="hidden" id="harborRegUrl" value="{{.HarborRegUrl}}">
<input type="hidden" id="public" value="{{.Public}}">
<input type="hidden" id="repoName" value="{{.RepoName}}">
<!-- tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane" id="tabRepoInfo">
<form class="form-inline">
<div class="form-group">
<label class="sr-only" for="txtRepoName">{{i18n .Lang "repo_name"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "repo_name"}}:</div>
<input type="text" class="form-control" id="txtRepoName">
<span class="input-group-btn">
<button id="btnSearchRepo" type="button" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</div>
</form>
<p>
<div class="table-responsive div-height">
<div class="alert alert-danger" role="alert" id="divErrMsg"><center></center></div>
<div class="panel-group" id="accordionRepo" role="tablist" aria-multiselectable="true">
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabUserInfo">
<form class="form-inline">
<div class="form-group">
<div class="input-group">
<label class="sr-only" for="txtSearchUser">{{i18n .Lang "username"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "username"}}:</div>
<input type="text" class="form-control" id="txtSearchUser">
<div class="col-md-10">
<input type="hidden" id="projectId" value="{{.ProjectId}}">
<input type="hidden" id="projectName" value="{{.ProjectName}}">
<input type="hidden" id="userId" value="{{.UserId}}">
<input type="hidden" id="ownerId" value="{{.OwnerId}}">
<input type="hidden" id="roleId" value="{{.RoleId}}">
<input type="hidden" id="harborRegUrl" value="{{.HarborRegUrl}}">
<input type="hidden" id="public" value="{{.Public}}">
<input type="hidden" id="repoName" value="{{.RepoName}}">
<!-- tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane" id="tabRepoInfo">
<form class="form-inline">
<div class="form-group">
<label class="sr-only" for="txtRepoName">{{i18n .Lang "repo_name"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "repo_name"}}:</div>
<input type="text" class="form-control" id="txtRepoName">
<span class="input-group-btn">
<button id="btnSearchUser" type="button" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button>
<button id="btnSearchRepo" type="button" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</div>
</form>
<p/>
<div class="table-responsive div-height">
<div class="alert alert-danger" role="alert" id="divErrMsg"><center></center></div>
<div class="panel-group" id="accordionRepo" role="tablist" aria-multiselectable="true">
</div>
</div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgUser" id="btnAddUser">{{i18n .Lang "add_members"}}</button>
</form>
<p>
<div class="table-responsive div-height">
<table id="tblUser" class="table table-hover">
<thead>
<tr>
<th>{{i18n .Lang "username"}}</th>
<th>{{i18n .Lang "role"}}</th>
<th>{{i18n .Lang "operation"}}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabOperationLog">
<form class="form-inline">
<div class="form-group">
<label for="txtUserName" class="sr-only">{{i18n .Lang "username"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "username"}}:</div>
<input type="text" class="form-control" id="txtSearchUserName">
<span class="input-group-btn">
<button id="btnFilterLog" type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgSearch"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</div>
<div class="form-group">
<div class="input-group">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapseAdvance" aria-expanded="false" aria-controls="collapseAdvance">{{i18n .Lang "advance"}}</button>
</div>
</div>
<form>
<p></p>
<div class="collapse" id="collapseAdvance">
<form class="form">
<div role="tabpanel" class="tab-pane" id="tabUserInfo">
<form class="form-inline">
<div class="form-group">
<label for="txtUserName" class="sr-only">{{i18n .Lang "operation"}}:</label>
<div class="input-group">
<label class="sr-only" for="txtSearchUser">{{i18n .Lang "username"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "username"}}:</div>
<input type="text" class="form-control" id="txtSearchUser">
<span class="input-group-btn">
<button id="btnSearchUser" type="button" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</div>
</div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgUser" id="btnAddUser">{{i18n .Lang "add_members"}}</button>
</form>
<p/>
<div class="table-responsive div-height">
<table id="tblUser" class="table table-hover">
<thead>
<tr>
<th>{{i18n .Lang "username"}}</th>
<th>{{i18n .Lang "role"}}</th>
<th>{{i18n .Lang "operation"}}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabOperationLog">
<form class="form-inline">
<div class="form-group">
<label for="txtUserName" class="sr-only">{{i18n .Lang "username"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "operation"}}:</div>
<span class="input-group-addon" id="spnFilterOption">
<input type="checkbox" name="chkAll" value="0"> {{i18n .Lang "all"}}
<input type="checkbox" name="chkOperation" value="create"> Create
<input type="checkbox" name="chkOperation" value="pull"> Pull
<input type="checkbox" name="chkOperation" value="push"> Push
<input type="checkbox" name="chkOperation" value="delete"> Delete
<input type="checkbox" name="chkOperation" value="others"> {{i18n .Lang "others"}}:
<input type="text" id="txtOthers" size="10">
<div class="input-group-addon">{{i18n .Lang "username"}}:</div>
<input type="text" class="form-control" id="txtSearchUserName">
<span class="input-group-btn">
<button id="btnFilterLog" type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgSearch"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</div>
<p></p>
<div class="form-group">
<label for="begindatepicker" class="sr-only">{{i18n .Lang "start_date"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "start_date"}}:</div>
<div class="input-group date" id="datetimepicker1">
<input type="text" class="form-control" id="begindatepicker" readonly="readonly">
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
<div class="input-group">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapseAdvance" aria-expanded="false" aria-controls="collapseAdvance">{{i18n .Lang "advance"}}</button>
</div>
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "end_date"}}:</div>
<div class="input-group date" id="datetimepicker2">
<input type="text" class="form-control" id="enddatepicker" readonly="readonly">
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
</div>
</div>
</form>
</div>
<div class="table-responsive div-height">
<table id="tblAccessLog" class="table table-hover" >
<thead>
<tr>
<th width="15%">{{i18n .Lang "username"}}</th>
<th width="30%">{{i18n .Lang "repo_name"}}</th>
<th width="15%">{{i18n .Lang "repo_tag"}}</th>
<th width="15%">{{i18n .Lang "operation"}}</th>
<th width="15%">{{i18n .Lang "timestamp"}}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<p/>
<div class="collapse" id="collapseAdvance">
<form class="form">
<div class="form-group">
<label for="txtUserName" class="sr-only">{{i18n .Lang "operation"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "operation"}}:</div>
<span class="input-group-addon" id="spnFilterOption">
<input type="checkbox" name="chkAll" value="0"> {{i18n .Lang "all"}}
<input type="checkbox" name="chkOperation" value="create"> Create
<input type="checkbox" name="chkOperation" value="pull"> Pull
<input type="checkbox" name="chkOperation" value="push"> Push
<input type="checkbox" name="chkOperation" value="delete"> Delete
<input type="checkbox" name="chkOperation" value="others"> {{i18n .Lang "others"}}:
<input type="text" id="txtOthers" size="10">
</span>
</div>
</div>
<p></p>
<div class="form-group">
<label for="begindatepicker" class="sr-only">{{i18n .Lang "start_date"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "start_date"}}:</div>
<div class="input-group date" id="datetimepicker1">
<input type="text" class="form-control" id="begindatepicker" readonly="readonly">
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
</div>
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "end_date"}}:</div>
<div class="input-group date" id="datetimepicker2">
<input type="text" class="form-control" id="enddatepicker" readonly="readonly">
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
</div>
</div>
</form>
</div>
<div class="table-responsive div-height">
<table id="tblAccessLog" class="table table-hover" >
<thead>
<tr>
<th width="15%">{{i18n .Lang "username"}}</th>
<th width="30%">{{i18n .Lang "repo_name"}}</th>
<th width="15%">{{i18n .Lang "repo_tag"}}</th>
<th width="15%">{{i18n .Lang "operation"}}</th>
<th width="15%">{{i18n .Lang "timestamp"}}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="dlgUser" tabindex="-1" role="dialog" aria-labelledby="User" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">

View File

@ -25,24 +25,24 @@
<!-- tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane" id="tabMyProject" style="margin-top: 15px;">
<form class="form-inline">
<label class="sr-only" for="txtProjectName">{{i18n .Lang "project_name"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "project_name"}}:</div>
<input type="text" class="form-control" id="txtSearchProject">
<span class="input-group-btn">
<button id="btnSearch" type="button" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgAddProject" id="btnAddProject">{{i18n .Lang "add_project"}}</button>
</form>
<form class="form-inline">
<label class="sr-only" for="txtProjectName">{{i18n .Lang "project_name"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "project_name"}}:</div>
<input type="text" class="form-control" id="txtSearchProject">
<span class="input-group-btn">
<button id="btnSearch" type="button" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#dlgAddProject" id="btnAddProject">{{i18n .Lang "add_project"}}</button>
</form>
<div class="table-responsive div-height">
<table id="tblProject" class="table table-hover">
<thead>
<tr>
<th width="35%">{{i18n .Lang "project_name"}}</th>
<th width="45%">{{i18n .Lang "creation_time"}}</th>
<th width="20%">{{i18n .Lang "publicity"}}</th>
<th width="20%">{{i18n .Lang "publicity"}}</th>
</tr>
</thead>
<tbody>
@ -51,65 +51,65 @@
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabAdminOption" style="visibility: hidden; margin-top: 15px;">
<form class="form-inline">
<label class="sr-only" for="txtProjectName">{{i18n .Lang "username"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "username"}}:</div>
<input type="text" class="form-control" id="txtSearchUsername">
<span class="input-group-btn">
<button id="btnSearchUsername" type="button" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</form>
<div class="table-responsive div-height">
<table id="tblUser" class="table table-hover">
<thead>
<tr>
<th width="35%">{{i18n .Lang "username"}}</th>
<th width="45%">{{i18n .Lang "email"}}</th>
<th width="20%">{{i18n .Lang "system_admin"}}</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<form class="form-inline">
<label class="sr-only" for="txtProjectName">{{i18n .Lang "username"}}:</label>
<div class="input-group">
<div class="input-group-addon">{{i18n .Lang "username"}}:</div>
<input type="text" class="form-control" id="txtSearchUsername">
<span class="input-group-btn">
<button id="btnSearchUsername" type="button" class="btn btn-primary"><span class="glyphicon glyphicon-search"></span></button>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="dlgAddProject" tabindex="-1" role="dialog" aria-labelledby="Add Project" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<a type="button" class="close" data-dismiss="modal" aria-label="Close" id="btnCancel">
<span aria-hidden="true">&times;</span>
</a>
<h4 class="modal-title" id="dlgAddProjectTitle">{{i18n .Lang "add_project"}}</h4>
</div>
<div class="modal-body">
<form role="form">
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="projectName" class="control-label">{{i18n .Lang "project_name"}}:</label>
<input type="text" class="form-control" id="projectName">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="isPublic" checked=false> {{i18n .Lang "check_for_publicity"}}
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="btnSave">{{i18n .Lang "button_save"}}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{i18n .Lang "button_cancel"}}</button>
</form>
<div class="table-responsive div-height">
<table id="tblUser" class="table table-hover">
<thead>
<tr>
<th width="35%">{{i18n .Lang "username"}}</th>
<th width="45%">{{i18n .Lang "email"}}</th>
<th width="20%">{{i18n .Lang "system_admin"}}</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="dlgAddProject" tabindex="-1" role="dialog" aria-labelledby="Add Project" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<a type="button" class="close" data-dismiss="modal" aria-label="Close" id="btnCancel">
<span aria-hidden="true">&times;</span>
</a>
<h4 class="modal-title" id="dlgAddProjectTitle">{{i18n .Lang "add_project"}}</h4>
</div>
<div class="modal-body">
<form role="form">
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="projectName" class="control-label">{{i18n .Lang "project_name"}}:</label>
<input type="text" class="form-control" id="projectName">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="isPublic" checked=false> {{i18n .Lang "check_for_publicity"}}
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="btnSave">{{i18n .Lang "button_save"}}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{i18n .Lang "button_cancel"}}</button>
</div>
</div>
</div>
</div>
</div>
<script src="static/resources/js/validate-options.js"></script>
<script src="static/resources/js/project.js"></script>

View File

@ -16,65 +16,65 @@
<div class="col-sm-4"></div>
<div class="col-sm-4">
<div class="page-header">
{{ if eq .IsAdmin true }}
<h1>{{i18n .Lang "add_user" }}</h1>
{{ else }}
<h1>{{i18n .Lang "registration"}}</h1>
{{ end }}
{{ if eq .IsAdmin true }}
<h1>{{i18n .Lang "add_user" }}</h1>
{{ else }}
<h1>{{i18n .Lang "registration"}}</h1>
{{ end }}
</div>
<form class="form">
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="username" class="control-label">{{i18n .Lang "username"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="text" class="form-control" id="Username">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "username_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="Email" class="control-label">{{i18n .Lang "email"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="email" class="form-control" id="Email">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "email_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="Realname" class="control-label">{{i18n .Lang "full_name"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="text" class="form-control" id="Realname">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "full_name_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="Password" class="control-label">{{i18n .Lang "password"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="password" class="form-control" id="Password">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="ConfirmedPassword" class="control-label">{{i18n .Lang "confirm_password"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="password" class="form-control" id="ConfirmedPassword">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="Comment" class="control-label">{{i18n .Lang "note_to_the_admin"}}</label>
<input type="text" class="form-control" id="Comment">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
</div>
<div class="form-group has-feedback">
<div class="text-center">
<button type="button" class="btn btn-default" id="btnPageSignUp">
{{ if eq .IsAdmin true }}
{{i18n .Lang "add_user" }}
{{ else }}
{{i18n .Lang "sign_up"}}
{{ end }}
</button>
</div>
</div>
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="username" class="control-label">{{i18n .Lang "username"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="text" class="form-control" id="Username">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "username_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="Email" class="control-label">{{i18n .Lang "email"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="email" class="form-control" id="Email">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "email_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="Realname" class="control-label">{{i18n .Lang "full_name"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="text" class="form-control" id="Realname">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "full_name_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="Password" class="control-label">{{i18n .Lang "password"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="password" class="form-control" id="Password">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="ConfirmedPassword" class="control-label">{{i18n .Lang "confirm_password"}}</label>
<p style="display:inline; color: red; font-size: 12pt;">*</p>
<input type="password" class="form-control" id="ConfirmedPassword">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="Comment" class="control-label">{{i18n .Lang "note_to_the_admin"}}</label>
<input type="text" class="form-control" id="Comment">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
</div>
<div class="form-group has-feedback">
<div class="text-center">
<button type="button" class="btn btn-default" id="btnPageSignUp">
{{ if eq .IsAdmin true }}
{{i18n .Lang "add_user" }}
{{ else }}
{{i18n .Lang "sign_up"}}
{{ end }}
</button>
</div>
</div>
</form>
</div>
<div class="col-sm-4"></div>

View File

@ -14,8 +14,8 @@
-->
<!DOCTYPE html>
<html>
<body>
<p>{{.Hint}}:</p>
<a href="{{.URL}}/resetPassword?reset_uuid={{.UUID}}">{{.URL}}/resetPassword?reset_uuid={{.UUID}}</a>
</body>
<body>
<p>{{.Hint}}:</p>
<a href="{{.URL}}/resetPassword?reset_uuid={{.UUID}}">{{.URL}}/resetPassword?reset_uuid={{.UUID}}</a>
</body>
</html>

View File

@ -17,27 +17,27 @@
<div class="col-sm-4"></div>
<div class="col-sm-4">
<div class="page-header">
<h1>{{i18n .Lang "title_reset_password"}}</h1>
<h1>{{i18n .Lang "title_reset_password"}}</h1>
</div>
<form class="form">
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="Password" class="control-label">{{i18n .Lang "password"}}</label>
<input type="password" class="form-control" id="Password">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="ConfirmedPassword" class="control-label">{{i18n .Lang "confirm_password"}}</label>
<input type="password" class="form-control" id="ConfirmedPassword">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<div class="text-center">
<button type="button" class="btn btn-default" id="btnSubmit">{{i18n .Lang "button_submit"}}</button>
</div>
</div>
<div class="alert alert-danger" role="alert" id="divErrMsg"></div>
<div class="form-group has-feedback">
<label for="Password" class="control-label">{{i18n .Lang "password"}}</label>
<input type="password" class="form-control" id="Password">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<label for="ConfirmedPassword" class="control-label">{{i18n .Lang "confirm_password"}}</label>
<input type="password" class="form-control" id="ConfirmedPassword">
<span class="glyphicon form-control-feedback" aria-hidden="true"></span>
<h6>{{i18n .Lang "password_description"}}</h6>
</div>
<div class="form-group has-feedback">
<div class="text-center">
<button type="button" class="btn btn-default" id="btnSubmit">{{i18n .Lang "button_submit"}}</button>
</div>
</div>
</form>
</div>
<div class="col-sm-4"></div>

View File

@ -18,15 +18,15 @@
<li><a href="/">{{i18n .Lang "home"}}</a></li>
<li>{{i18n .Lang "search"}}</li>
</ol>
<div class="panel panel-default">
<div class="panel-heading" id="panelCommonSearchProjectsHeader">{{i18n .Lang "projects"}}</div>
<div class="panel-body" id="panelCommonSearchProjectsBody">
</div>
<div class="panel panel-default">
<div class="panel-heading" id="panelCommonSearchProjectsHeader">{{i18n .Lang "projects"}}</div>
<div class="panel-body" id="panelCommonSearchProjectsBody">
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" id="panelCommonSearchRepositoriesHeader">{{i18n .Lang "repositories"}}</div>
<div class="panel-body" id="panelCommonSearchRepositoriesBody">
</div>
<div class="panel-heading" id="panelCommonSearchRepositoriesHeader">{{i18n .Lang "repositories"}}</div>
<div class="panel-body" id="panelCommonSearchRepositoriesBody">
</div>
</div>
</div>
<script src="static/resources/js/search.js"></script>

View File

@ -14,15 +14,15 @@
-->
<!DOCTYPE html>
<html>
<head>
{{.HeaderInc}}
<title>{{.PageTitle}}</title>
</head>
<body>
{{.HeaderContent}}
{{.BodyContent}}
{{.FooterInc}}
{{.ModalDialog}}
{{.FootContent}}
</body>
<head>
{{.HeaderInc}}
<title>{{.PageTitle}}</title>
</head>
<body>
{{.HeaderContent}}
{{.BodyContent}}
{{.FooterInc}}
{{.ModalDialog}}
{{.FootContent}}
</body>
</html>

View File

@ -13,11 +13,11 @@
limitations under the License.
-->
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-md-5 col-md-offset-4">
<p class="text-muted">{{i18n .Lang "copyright"}} © 2015-2016 VMware, Inc. {{i18n .Lang "all_rights_reserved"}}</p>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-5 col-md-offset-4">
<p class="text-muted">{{i18n .Lang "copyright"}} © 2015-2016 VMware, Inc. {{i18n .Lang "all_rights_reserved"}}</p>
</div>
</div>
</div>
</footer>

View File

@ -11,79 +11,83 @@
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.
-->
-->
<input type="hidden" id="currentLanguage" value="{{.Lang}}">
<input type="hidden" id="isAdmin" value="{{.IsAdmin}}">
<nav class="navbar navbar-default" role="navigation" style="margin-bottom: 0;">
<div class="navbar-header">
<button aria-controls="navbar" aria-expanded="false" data-target="#navbar" data-toggle="collapse" class="navbar-toggle collapsed" type="button">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><img src="static/resources/image/Harbor_Logo_rec.png" height="40px" width="80px"/></a>
</div>
</div>
<div id="navbar" class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<div class="form-group">
<div class="input-group">
<ul class="nav navbar-nav">
<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>
{{i18n .Lang "language"}}
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/language?lang=en-US">{{i18n .Lang "language_en-US"}}</a></li>
<li><a href="/language?lang=zh-CN">{{i18n .Lang "language_zh-CN"}}</a></li>
<li><a href="/language?lang=de-DE">{{i18n .Lang "language_de-DE"}}</a></li>
<li><a href="/language?lang=ru-RU">{{i18n .Lang "language_ru-RU"}}</a></li>
</ul>
</li>
</ul>
</div>
<div class="input-group" >
<span class="input-group-addon"><span class="input-group glyphicon glyphicon-search"></span></span>
<input type="text" class="form-control" id="txtCommonSearch" size="50" placeholder="{{i18n .Lang "search_placeholder"}}">
</div>
</div>
{{ if .Username }}
<div class="input-group">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span> {{.Username}}<span class="caret"></span></a>
<ul class="dropdown-menu">
{{ if eq .AuthMode "db_auth" }}
<li><a id="aChangePassword" href="/changePassword" target="_blank"><span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;{{i18n .Lang "change_password"}}</a></li>
<li role="separator" class="divider"></li>
{{ end }}
{{ if eq .IsLdapAdminUser true }}
<li><a id="aChangePassword" href="/changePassword" target="_blank"><span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;{{i18n .Lang "change_password"}}</a></li>
<li role="separator" class="divider"></li>
{{ end }}
{{ if eq .AuthMode "db_auth" }}
{{ if eq .IsAdmin true }}
<li><a id="aAddUser" href="/addUser" target="_blank"><span class="glyphicon glyphicon-plus"></span>&nbsp;&nbsp;{{i18n .Lang "add_user"}}</a></li>
{{ end }}
{{ end}}
<li><a id="aLogout" href="#"><span class="glyphicon glyphicon-log-in"></span>&nbsp;&nbsp;{{i18n .Lang "log_out"}}</a></li>
<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>
{{i18n .Lang "language"}}
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/language?lang=en-US">{{i18n .Lang "language_en-US"}}</a></li>
<li><a href="/language?lang=zh-CN">{{i18n .Lang "language_zh-CN"}}</a></li>
<li><a href="/language?lang=de-DE">{{i18n .Lang "language_de-DE"}}</a></li>
<li><a href="/language?lang=ru-RU">{{i18n .Lang "language_ru-RU"}}</a></li>
<li><a href="/language?lang=ja-JP">{{i18n .Lang "language_ja-JP"}}</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
{{ else if eq .AuthMode "db_auth" }}
<div class="input-group">
&nbsp;<button type="button" class="btn btn-default" id="btnSignIn">{{i18n .Lang "sign_in"}}</button>
{{ if eq .SelfRegistration true }}
&nbsp;<button type="button" class="btn btn-success" id="btnSignUp">{{i18n .Lang "sign_up"}}</button>
</div>
<div class="input-group" >
<span class="input-group-addon"><span class="input-group glyphicon glyphicon-search"></span></span>
<input type="text" class="form-control" id="txtCommonSearch" size="50" placeholder="{{i18n .Lang "search_placeholder"}}">
</div>
</div>
{{ if .Username }}
<div class="input-group">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span> {{.Username}}<span class="caret"></span></a>
<ul class="dropdown-menu">
{{ if eq .AuthMode "db_auth" }}
<li><a id="aChangePassword" href="/changePassword" target="_blank"><span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;{{i18n .Lang "change_password"}}</a></li>
<li role="separator" class="divider"></li>
{{ end }}
{{ if eq .IsLdapAdminUser true }}
<li><a id="aChangePassword" href="/changePassword" target="_blank"><span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;{{i18n .Lang "change_password"}}</a></li>
<li role="separator" class="divider"></li>
{{ end }}
{{ if eq .AuthMode "db_auth" }}
{{ if eq .IsAdmin true }}
<li><a id="aAddUser" href="/addUser" target="_blank"><span class="glyphicon glyphicon-plus"></span>&nbsp;&nbsp;{{i18n .Lang "add_user"}}</a></li>
{{ end }}
{{ end}}
<li><a id="aLogout" href="#"><span class="glyphicon glyphicon-log-in"></span>&nbsp;&nbsp;{{i18n .Lang "log_out"}}</a></li>
</ul>
</li>
</ul>
</div>
{{ else if eq .AuthMode "db_auth" }}
<div class="input-group">
&nbsp;<button type="button" class="btn btn-default" id="btnSignIn">{{i18n .Lang "sign_in"}}</button>
{{ if eq .SelfRegistration true }}
&nbsp;<button type="button" class="btn btn-success" id="btnSignUp">{{i18n .Lang "sign_up"}}</button>
{{ end }}
</div>
{{ else }}
<div class="input-group">
&nbsp;<button type="button" class="btn btn-default" id="btnSignIn">{{i18n .Lang "sign_in"}}</button>
</div>
{{ end }}
</div>
{{ else }}
<div class="input-group">
&nbsp;<button type="button" class="btn btn-default" id="btnSignIn">{{i18n .Lang "sign_in"}}</button>
</div>
{{ end }}
</form>
</div>
</nav>
</form>
</div>
</nav>

View File

@ -13,26 +13,26 @@
limitations under the License.
-->
<style>
.center {
margin-left: auto;
margin-right: auto;
top: 10%;
}
.center {
margin-left: auto;
margin-right: auto;
top: 10%;
}
</style>
<!-- Modal -->
<div class="center modal fade" id="dlgModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="dlgLabel"></h4>
</div>
<div class="modal-body" id="dlgBody">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="dlgConfirm" data-dismiss="modal">{{i18n .Lang "dlg_button_ok"}}</button>
<button type="button" class="btn btn-primary" id="dlgCancel" data-dismiss="modal" style="display: none;">{{i18n .Lang "dlg_button_cancel"}}</button>
</div>
</div>
</div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="dlgLabel"></h4>
</div>
<div class="modal-body" id="dlgBody">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="dlgConfirm" data-dismiss="modal">{{i18n .Lang "dlg_button_ok"}}</button>
<button type="button" class="btn btn-primary" id="dlgCancel" data-dismiss="modal" style="display: none;">{{i18n .Lang "dlg_button_cancel"}}</button>
</div>
</div>
</div>
</div>

View File

@ -13,28 +13,28 @@
limitations under the License.
-->
<div class="container">
<form class="form-signin form-horizontal">
<div class="form-group">
<label for="Principal" class="col-md-4 control-label">{{i18n .Lang "username_email"}}</label>
<div class="col-md-8">
<input type="text" id="Principal" class="form-control" placeholder="{{i18n .Lang "username_email"}}">
<form class="form-signin form-horizontal">
<div class="form-group">
<label for="Principal" class="col-md-4 control-label">{{i18n .Lang "username_email"}}</label>
<div class="col-md-8">
<input type="text" id="Principal" class="form-control" placeholder="{{i18n .Lang "username_email"}}">
</div>
</div>
</div>
<div class="form-group">
<label for="Password" class="col-md-4 control-label">{{i18n .Lang "password"}}</label>
<div class="col-md-8">
<input type="password" id="Password" class="form-control" placeholder="{{i18n .Lang "password"}}">
<div class="form-group">
<label for="Password" class="col-md-4 control-label">{{i18n .Lang "password"}}</label>
<div class="col-md-8">
<input type="password" id="Password" class="form-control" placeholder="{{i18n .Lang "password"}}">
</div>
</div>
</div>
<button class="btn btn-lg btn-primary btn-block" type="button" id="btnPageSignIn">{{i18n .Lang "sign_in"}}</button>
{{ if eq .AuthMode "db_auth" }}
<div class="form-group">
<div class="col-md-12">
<button type="button" class="btn btn-link pull-right" id="btnForgot">{{i18n .Lang "forgot_password"}}</button>
</div>
</div>
{{ end }}
</form>
<button class="btn btn-lg btn-primary btn-block" type="button" id="btnPageSignIn">{{i18n .Lang "sign_in"}}</button>
{{ if eq .AuthMode "db_auth" }}
<div class="form-group">
<div class="col-md-12">
<button type="button" class="btn btn-link pull-right" id="btnForgot">{{i18n .Lang "forgot_password"}}</button>
</div>
</div>
{{ end }}
</form>
</div>
<link href="static/resources/css/sign-in.css" type="text/css" rel="stylesheet">
<script src="static/resources/js/sign-in.js"></script>