mirror of
https://github.com/goharbor/harbor.git
synced 2025-02-21 14:21:36 +01:00
Merge latest updates.
This commit is contained in:
parent
a80008d0f9
commit
5c3fd9fc43
@ -31,6 +31,7 @@ env:
|
||||
|
||||
before_install:
|
||||
- sudo ./tests/hostcfg.sh
|
||||
- sudo ./tests/generateCerts.sh
|
||||
- sudo ./make/prepare
|
||||
|
||||
install:
|
||||
@ -92,12 +93,12 @@ script:
|
||||
- goveralls -coverprofile=profile.cov -service=travis-ci
|
||||
|
||||
- docker-compose -f make/docker-compose.test.yml down
|
||||
- sudo make/prepare
|
||||
- sudo rm -rf /data/config/*
|
||||
- docker-compose -f make/dev/docker-compose.yml up -d
|
||||
- sudo rm -rf /data/config/*
|
||||
- ls /data/cert
|
||||
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true
|
||||
|
||||
- docker ps
|
||||
- go run tests/startuptest.go http://localhost/
|
||||
- go run tests/startuptest.go https://localhost/
|
||||
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}
|
||||
|
||||
# - sudo ./tests/testprepare.sh
|
||||
|
37
Makefile
37
Makefile
@ -79,6 +79,13 @@ REGISTRYSERVER=
|
||||
REGISTRYPROJECTNAME=vmware
|
||||
DEVFLAG=true
|
||||
NOTARYFLAG=false
|
||||
REGISTRYVERSION=2.6.0
|
||||
NGINXVERSION=1.11.5
|
||||
PHOTONVERSION=1.0
|
||||
NOTARYVERSION=server-0.5.0-fix
|
||||
NOTARYSIGNERVERSION=signer-0.5.0
|
||||
MARIADBVERSION=10.1.10
|
||||
HTTPPROXY=
|
||||
|
||||
#clarity parameters
|
||||
CLARITYIMAGE=danieljt/harbor-clarity-base[:tag]
|
||||
@ -206,7 +213,11 @@ compile_jobservice:
|
||||
|
||||
compile_clarity:
|
||||
@echo "compiling binary for clarity ui..."
|
||||
@$(DOCKERCMD) run --rm -v $(UIPATH)/static/new-ui:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src -v $(UINGPATH)/src/app:$(CLARITYSEEDPATH)/src/app $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT)
|
||||
@if [ "$(HTTPPROXY)" != "" ] ; then \
|
||||
$(DOCKERCMD) run --rm -v $(UIPATH)/static:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT) -p $(HTTPPROXY); \
|
||||
else \
|
||||
$(DOCKERCMD) run --rm -v $(UIPATH)/static:$(CLARITYSEEDPATH)/dist -v $(UINGPATH)/src:$(CLARITYSEEDPATH)/src $(CLARITYIMAGE) $(SHELL) $(CLARITYBUILDSCRIPT); \
|
||||
fi
|
||||
@echo "Done."
|
||||
|
||||
compile_normal: compile_clarity compile_adminserver compile_ui compile_jobservice
|
||||
@ -290,13 +301,13 @@ package_offline: compile build modify_composefile
|
||||
@cp NOTICE $(HARBORPKG)/NOTICE
|
||||
|
||||
@echo "pulling nginx and registry..."
|
||||
@$(DOCKERPULL) registry:2.5.1
|
||||
@$(DOCKERPULL) nginx:1.11.5
|
||||
@$(DOCKERPULL) registry:$(REGISTRYVERSION)
|
||||
@$(DOCKERPULL) nginx:$(NGINXVERSION)
|
||||
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
|
||||
echo "pulling notary and mariadb..."; \
|
||||
$(DOCKERPULL) jiangd/notary:server-0.5.0-fix; \
|
||||
$(DOCKERPULL) notary:signer-0.5.0; \
|
||||
$(DOCKERPULL) mariadb:10.1.10; \
|
||||
$(DOCKERPULL) jiangd/notary:$(NOTARYVERSION); \
|
||||
$(DOCKERPULL) notary:$(NOTARYSIGNERVERSION); \
|
||||
$(DOCKERPULL) mariadb:$(MARIADBVERSION); \
|
||||
fi
|
||||
|
||||
@echo "saving harbor docker image"
|
||||
@ -307,8 +318,8 @@ package_offline: compile build modify_composefile
|
||||
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
|
||||
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
|
||||
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
|
||||
nginx:1.11.5 registry:2.5.1 photon:1.0 \
|
||||
jiangd/notary:server-0.5.0-fix notary:signer-0.5.0 mariadb:10.1.10; \
|
||||
nginx:$(NGINXVERSION) registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) \
|
||||
jiangd/notary:$(NOTARYVERSION) notary:$(NOTARYSIGNERVERSION) mariadb:$(MARIADBVERSION); \
|
||||
else \
|
||||
$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
|
||||
$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
|
||||
@ -316,7 +327,7 @@ package_offline: compile build modify_composefile
|
||||
$(DOCKERIMAGENAME_LOG):$(VERSIONTAG) \
|
||||
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
|
||||
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
|
||||
nginx:1.11.5 registry:2.5.1 photon:1.0 ; \
|
||||
nginx:$(NGINXVERSION) registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) ; \
|
||||
fi
|
||||
|
||||
@if [ "$(NOTARYFLAG)" = "true" ] ; then \
|
||||
@ -324,14 +335,14 @@ package_offline: compile build modify_composefile
|
||||
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
|
||||
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
|
||||
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
|
||||
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) ; \
|
||||
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
|
||||
$(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) ; \
|
||||
else \
|
||||
$(TARCMD) -zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
|
||||
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
|
||||
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
|
||||
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
|
||||
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
|
||||
$(HARBORPKG)/$(DOCKERCOMPOSENOTARYFILENAME) ; \
|
||||
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) ; \
|
||||
fi
|
||||
|
||||
@rm -rf $(HARBORPKG)
|
||||
@ -400,7 +411,7 @@ cleanimage:
|
||||
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_DB):$(VERSIONTAG)
|
||||
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG)
|
||||
- $(DOCKERRMIMAGE) -f $(DOCKERIMAGENAME_LOG):$(VERSIONTAG)
|
||||
# - $(DOCKERRMIMAGE) -f registry:2.5.1
|
||||
# - $(DOCKERRMIMAGE) -f registry:$(REGISTRYVERSION)
|
||||
# - $(DOCKERRMIMAGE) -f nginx:1.11.5
|
||||
|
||||
cleandockercomposefile:
|
||||
|
@ -1529,6 +1529,22 @@ paths:
|
||||
description: User does not have permission of admin role.
|
||||
500:
|
||||
description: Unexpected internal errors.
|
||||
/configurations/reset:
|
||||
post:
|
||||
summary: Reset system configurations.
|
||||
description: |
|
||||
Reset system configurations from environment variables. Can only be accessed by admin user.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
200:
|
||||
description: Reset system configurations successfully.
|
||||
401:
|
||||
description: User need to log in first.
|
||||
403:
|
||||
description: User does not have permission of admin role.
|
||||
500:
|
||||
description: Unexpected internal errors.
|
||||
/email/ping:
|
||||
post:
|
||||
summary: Test connection and authentication with email server.
|
||||
@ -2009,6 +2025,9 @@ definitions:
|
||||
self_registration:
|
||||
type: boolean
|
||||
description: Indicate whether the Harbor instance enable user to register himself.
|
||||
has_ca_root:
|
||||
type: boolean
|
||||
description: Indicate whether there is a ca root cert file ready for download in the file system.
|
||||
SystemInfo:
|
||||
type: object
|
||||
properties:
|
||||
@ -2082,7 +2101,7 @@ definitions:
|
||||
type: string
|
||||
description: The host of email server.
|
||||
email_port:
|
||||
type: string
|
||||
type: integer
|
||||
description: The port of email server.
|
||||
email_username:
|
||||
type: string
|
||||
@ -2091,7 +2110,7 @@ definitions:
|
||||
type: string
|
||||
description: The password of email server.
|
||||
email_ssl:
|
||||
type: string
|
||||
type: boolean
|
||||
description: Use ssl/tls or not.
|
||||
email_identity:
|
||||
type: string
|
||||
@ -2146,4 +2165,4 @@ definitions:
|
||||
description: The creation time of repository.
|
||||
update_time:
|
||||
type: string
|
||||
description: The update time of repository.
|
||||
description: The update time of repository.
|
||||
|
@ -191,14 +191,14 @@ Run the below commands on the host which Harbor is deployed on to preview what f
|
||||
|
||||
```sh
|
||||
$ docker-compose stop
|
||||
$ docker run -it --name gc --rm --volumes-from registry registry:2.5.1 garbage-collect --dry-run /etc/registry/config.yml
|
||||
$ docker run -it --name gc --rm --volumes-from registry registry:2.6.0 garbage-collect --dry-run /etc/registry/config.yml
|
||||
```
|
||||
**NOTE:** The above option "--dry-run" will print the progress without removing any data.
|
||||
|
||||
Verify the result of the above test, then use the below commands to perform garbage collection and restart Harbor.
|
||||
|
||||
```sh
|
||||
$ docker run -it --name gc --rm --volumes-from registry registry:2.5.1 garbage-collect /etc/registry/config.yml
|
||||
$ docker run -it --name gc --rm --volumes-from registry registry:2.6.0 garbage-collect /etc/registry/config.yml
|
||||
$ docker-compose start
|
||||
```
|
||||
|
||||
|
@ -37,3 +37,4 @@ USE_COMPRESSED_JS=$use_compressed_js
|
||||
GODEBUG=netdns=cgo
|
||||
ADMIRAL_URL=$admiral_url
|
||||
WITH_NOTARY=$with_notary
|
||||
RESET=false
|
||||
|
@ -8,7 +8,7 @@ events {
|
||||
|
||||
http {
|
||||
tcp_nodelay on;
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/conf.d/*.upstream.conf;
|
||||
|
||||
# this is necessary for us to be able to disable request buffering in all cases
|
||||
proxy_http_version 1.1;
|
||||
@ -42,6 +42,8 @@ http {
|
||||
# required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486)
|
||||
chunked_transfer_encoding on;
|
||||
|
||||
include /etc/nginx/conf.d/*.location.conf;
|
||||
|
||||
location / {
|
||||
proxy_pass http://ui/;
|
||||
proxy_set_header Host $$http_host;
|
||||
@ -62,19 +64,6 @@ http {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location /notary/v2/ {
|
||||
proxy_pass http://notary-server/v2/;
|
||||
proxy_set_header Host $$http_host;
|
||||
proxy_set_header X-Real-IP $$remote_addr;
|
||||
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
|
||||
|
||||
# When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
|
||||
proxy_set_header X-Forwarded-Proto $$scheme;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /v2/ {
|
||||
proxy_pass http://registry/v2/;
|
||||
proxy_set_header Host $$http_host;
|
||||
|
12
make/common/templates/nginx/notary.location.conf
Normal file
12
make/common/templates/nginx/notary.location.conf
Normal file
@ -0,0 +1,12 @@
|
||||
location /notary/v2/ {
|
||||
proxy_pass http://notary-server/v2/;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings.
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
@ -10,7 +10,7 @@ services:
|
||||
ports:
|
||||
- 1514:514
|
||||
registry:
|
||||
image: library/registry:2.5.1
|
||||
image: library/registry:2.6.0
|
||||
restart: always
|
||||
volumes:
|
||||
- /data/registry:/storage
|
||||
|
@ -1,12 +1,18 @@
|
||||
FROM node:7.5.0
|
||||
|
||||
COPY angular-cli.json /
|
||||
COPY index.html /
|
||||
COPY entrypoint.sh /
|
||||
RUN mkdir -p /clarity-seed
|
||||
|
||||
RUN npm install -g @angular/cli && \
|
||||
chmod u+x entrypoint.sh
|
||||
COPY src/ui_ng/package.json /clarity-seed
|
||||
COPY src/ui_ng/tslint.json /clarity-seed
|
||||
COPY src/ui_ng/typings.json /clarity-seed
|
||||
COPY src/ui_ng/yarn.lock /clarity-seed
|
||||
COPY make/dev/nodeclarity/angular-cli.json /clarity-seed
|
||||
COPY make/dev/nodeclarity/entrypoint.sh /
|
||||
|
||||
VOLUME ["/clarity-seed", "/clarity-seed/dist"]
|
||||
WORKDIR /clarity-seed
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
RUN npm install -g @angular/cli && \
|
||||
npm install && \
|
||||
chmod u+x /entrypoint.sh
|
||||
|
||||
VOLUME ["/clarity-seed", "/clarity-seed/dist"]
|
@ -1,11 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd /clarity-seed
|
||||
rm -rf dist/*
|
||||
cp /angular-cli.json /clarity-seed
|
||||
|
||||
npm_proxy=
|
||||
|
||||
while getopts p: option
|
||||
do
|
||||
case "${option}"
|
||||
in
|
||||
p) npm_proxy=${OPTARG};;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -z "$npm_proxy" -a "$npm_proxy" != " " ]; then
|
||||
npm config set proxy $npm_proxy
|
||||
fi
|
||||
|
||||
npm install
|
||||
ng build
|
||||
|
||||
cp /index.html dist/index.html
|
||||
cp -r ./src/i18n/ dist/
|
||||
|
||||
|
@ -11,7 +11,7 @@ services:
|
||||
networks:
|
||||
- harbor
|
||||
registry:
|
||||
image: registry:2.5.1
|
||||
image: registry:2.6.0
|
||||
container_name: registry
|
||||
restart: always
|
||||
volumes:
|
||||
|
14
make/prepare
14
make/prepare
@ -147,7 +147,10 @@ token_expiration = rcp.get("configuration", "token_expiration")
|
||||
verify_remote_cert = rcp.get("configuration", "verify_remote_cert")
|
||||
proj_cre_restriction = rcp.get("configuration", "project_creation_restriction")
|
||||
secretkey_path = rcp.get("configuration", "secretkey_path")
|
||||
admiral_url = rcp.get("configuration", "admiral_url")
|
||||
if rcp.has_option("configuration", "admiral_url"):
|
||||
admiral_url = rcp.get("configuration", "admiral_url")
|
||||
else:
|
||||
admiral_url = ""
|
||||
secret_key = get_secret_key(secretkey_path)
|
||||
########
|
||||
|
||||
@ -313,20 +316,21 @@ if args.notary_mode:
|
||||
shutil.rmtree(os.path.join(notary_config_dir, "mysql-initdb.d"))
|
||||
shutil.copytree(os.path.join(notary_temp_dir, "mysql-initdb.d"), os.path.join(notary_config_dir, "mysql-initdb.d"))
|
||||
#TODO:generate certs?
|
||||
print ("Copying certs for notary signer")
|
||||
print("Copying certs for notary signer")
|
||||
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer.crt"), notary_config_dir)
|
||||
shutil.copy2(os.path.join(notary_temp_dir, "notary-signer.key"), notary_config_dir)
|
||||
shutil.copy2(os.path.join(notary_temp_dir, "root-ca.crt"), notary_config_dir)
|
||||
|
||||
shutil.copy2(os.path.join(registry_config_dir, "root.crt"), notary_config_dir)
|
||||
print ("Copying notary signer configuration file")
|
||||
print("Copying notary signer configuration file")
|
||||
shutil.copy2(os.path.join(notary_temp_dir, "signer-config.json"), notary_config_dir)
|
||||
render(os.path.join(notary_temp_dir, "server-config.json"),
|
||||
os.path.join(notary_config_dir, "server-config.json"),
|
||||
token_endpoint=ui_url)
|
||||
|
||||
print ("Copying nginx configuration file for notary")
|
||||
shutil.copy2(os.path.join(templates_dir, "nginx", "nginx.notary.conf"), nginx_conf_d)
|
||||
print("Copying nginx configuration file for notary")
|
||||
shutil.copy2(os.path.join(templates_dir, "nginx", "notary.upstream.conf"), nginx_conf_d)
|
||||
shutil.copy2(os.path.join(templates_dir, "nginx", "notary.location.conf"), nginx_conf_d)
|
||||
|
||||
default_alias = ''.join(random.choice(string.ascii_letters) for i in range(8))
|
||||
render(os.path.join(notary_temp_dir, "signer_env"), os.path.join(notary_config_dir, "signer_env"), alias = default_alias)
|
||||
|
@ -16,6 +16,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -32,3 +33,17 @@ func handleUnauthorized(w http.ResponseWriter) {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized),
|
||||
http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// response status code will be written automatically if there is an error
|
||||
func writeJSON(w http.ResponseWriter, v interface{}) error {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
handleInternalServerError(w)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -19,40 +19,13 @@ import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
cfg "github.com/vmware/harbor/src/adminserver/systemcfg"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
func isAuthenticated(r *http.Request) (bool, error) {
|
||||
uiSecret := os.Getenv("UI_SECRET")
|
||||
jobserviceSecret := os.Getenv("JOBSERVICE_SECRET")
|
||||
c, err := r.Cookie("secret")
|
||||
if err != nil {
|
||||
if err == http.ErrNoCookie {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return c != nil && (c.Value == uiSecret ||
|
||||
c.Value == jobserviceSecret), nil
|
||||
}
|
||||
|
||||
// ListCfgs lists configurations
|
||||
func ListCfgs(w http.ResponseWriter, r *http.Request) {
|
||||
authenticated, err := isAuthenticated(r)
|
||||
if err != nil {
|
||||
log.Errorf("failed to check whether the request is authenticated or not: %v", err)
|
||||
handleInternalServerError(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
handleUnauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := cfg.GetSystemCfg()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get system configurations: %v", err)
|
||||
@ -60,31 +33,14 @@ func ListCfgs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal configurations: %v", err)
|
||||
handleInternalServerError(w)
|
||||
return
|
||||
}
|
||||
if _, err = w.Write(b); err != nil {
|
||||
if err = writeJSON(w, cfg); err != nil {
|
||||
log.Errorf("failed to write response: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateCfgs updates configurations
|
||||
func UpdateCfgs(w http.ResponseWriter, r *http.Request) {
|
||||
authenticated, err := isAuthenticated(r)
|
||||
if err != nil {
|
||||
log.Errorf("failed to check whether the request is authenticated or not: %v", err)
|
||||
handleInternalServerError(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
handleUnauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Errorf("failed to read request body: %v", err)
|
||||
@ -104,3 +60,12 @@ func UpdateCfgs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ResetCfgs resets configurations from environment variables
|
||||
func ResetCfgs(w http.ResponseWriter, r *http.Request) {
|
||||
if err := cfg.Reset(); err != nil {
|
||||
log.Errorf("failed to reset system configurations: %v", err)
|
||||
handleInternalServerError(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func TestConfigAPI(t *testing.T) {
|
||||
|
||||
secret := "secret"
|
||||
envs := map[string]string{
|
||||
|
||||
"AUTH_MODE": comcfg.DBAuth,
|
||||
"JSON_CFG_STORE_PATH": configPath,
|
||||
"KEY_PATH": secretKeyPath,
|
||||
"UI_SECRET": secret,
|
||||
@ -79,19 +79,12 @@ func TestConfigAPI(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ListCfgs(w, r)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: "secret",
|
||||
Value: secret,
|
||||
})
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
w := httptest.NewRecorder()
|
||||
ListCfgs(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
|
||||
@ -164,7 +157,55 @@ func TestConfigAPI(t *testing.T) {
|
||||
|
||||
mode := m[comcfg.AUTHMode].(string)
|
||||
if mode != comcfg.LDAPAuth {
|
||||
t.Errorf("unexpected ldap scope: %s != %s", mode, comcfg.LDAPAuth)
|
||||
t.Errorf("unexpected auth mode: %s != %s", mode, comcfg.LDAPAuth)
|
||||
return
|
||||
}
|
||||
|
||||
// reset configurations
|
||||
w = httptest.NewRecorder()
|
||||
r, err = http.NewRequest("POST", "", nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create request: %v", err)
|
||||
return
|
||||
}
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: "secret",
|
||||
Value: secret,
|
||||
})
|
||||
|
||||
ResetCfgs(w, r)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// confirm the reset
|
||||
r, err = http.NewRequest("GET", "", nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create request: %v", err)
|
||||
return
|
||||
}
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: "secret",
|
||||
Value: secret,
|
||||
})
|
||||
w = httptest.NewRecorder()
|
||||
ListCfgs(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
m, err = parse(w.Body)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse response body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
mode = m[comcfg.AUTHMode].(string)
|
||||
if mode != comcfg.DBAuth {
|
||||
t.Errorf("unexpected auth mode: %s != %s", mode, comcfg.LDAPAuth)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
38
src/adminserver/api/systeminfo.go
Normal file
38
src/adminserver/api/systeminfo.go
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// Capacity handles /api/systeminfo/capacity and returns system capacity
|
||||
func Capacity(w http.ResponseWriter, r *http.Request) {
|
||||
capacity, err := imagestorage.GlobalDriver.Cap()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get capacity: %v", err)
|
||||
handleInternalServerError(w)
|
||||
return
|
||||
}
|
||||
|
||||
if err = writeJSON(w, capacity); err != nil {
|
||||
log.Errorf("failed to write response: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
74
src/adminserver/api/systeminfo_test.go
Normal file
74
src/adminserver/api/systeminfo_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
|
||||
)
|
||||
|
||||
type fakeImageStorageDriver struct {
|
||||
capacity *imagestorage.Capacity
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeImageStorageDriver) Name() string {
|
||||
return "fake"
|
||||
}
|
||||
|
||||
func (f *fakeImageStorageDriver) Cap() (*imagestorage.Capacity, error) {
|
||||
return f.capacity, f.err
|
||||
}
|
||||
|
||||
func TestCapacity(t *testing.T) {
|
||||
cases := []struct {
|
||||
driver imagestorage.Driver
|
||||
responseCode int
|
||||
capacity *imagestorage.Capacity
|
||||
}{
|
||||
{&fakeImageStorageDriver{nil, errors.New("error")}, http.StatusInternalServerError, nil},
|
||||
{&fakeImageStorageDriver{&imagestorage.Capacity{100, 90}, nil}, http.StatusOK, &imagestorage.Capacity{100, 90}},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
for _, c := range cases {
|
||||
imagestorage.GlobalDriver = c.driver
|
||||
w := httptest.NewRecorder()
|
||||
Capacity(w, req)
|
||||
assert.Equal(t, c.responseCode, w.Code, "unexpected response code")
|
||||
if c.responseCode == http.StatusOK {
|
||||
b, err := ioutil.ReadAll(w.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read from response body: %v", err)
|
||||
}
|
||||
capacity := &imagestorage.Capacity{}
|
||||
if err = json.Unmarshal(b, capacity); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
assert.Equal(t, c.capacity, capacity)
|
||||
}
|
||||
}
|
||||
}
|
65
src/adminserver/auth/auth.go
Normal file
65
src/adminserver/auth/auth.go
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Authenticator defines Authenticate function to authenticate requests
|
||||
type Authenticator interface {
|
||||
// Authenticate the request, if there is no error, the bool value
|
||||
// determines whether the request is authenticated or not
|
||||
Authenticate(req *http.Request) (bool, error)
|
||||
}
|
||||
|
||||
type secretAuthenticator struct {
|
||||
secrets map[string]string
|
||||
}
|
||||
|
||||
// NewSecretAuthenticator returns an instance of secretAuthenticator
|
||||
func NewSecretAuthenticator(secrets map[string]string) Authenticator {
|
||||
return &secretAuthenticator{
|
||||
secrets: secrets,
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate the request according the secret
|
||||
func (s *secretAuthenticator) Authenticate(req *http.Request) (bool, error) {
|
||||
if len(s.secrets) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
secret, err := req.Cookie("secret")
|
||||
if err != nil {
|
||||
if err == http.ErrNoCookie {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if secret == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, v := range s.secrets {
|
||||
if secret.Value == v {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
57
src/adminserver/auth/auth_test.go
Normal file
57
src/adminserver/auth/auth_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
secret := "correct"
|
||||
req1, err := http.NewRequest("", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req2, err := http.NewRequest("", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request: %v", err)
|
||||
}
|
||||
req2.AddCookie(&http.Cookie{
|
||||
Name: "secret",
|
||||
Value: secret,
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
secrets map[string]string
|
||||
req *http.Request
|
||||
result bool
|
||||
}{
|
||||
{nil, req1, true},
|
||||
{map[string]string{"secret1": "incorrect"}, req2, false},
|
||||
{map[string]string{"secret1": "incorrect", "secret2": secret}, req2, true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
authenticator := NewSecretAuthenticator(c.secrets)
|
||||
authenticated, err := authenticator.Authenticate(c.req)
|
||||
assert.Nil(t, err, "unexpected error")
|
||||
assert.Equal(t, c.result, authenticated, "unexpected result")
|
||||
}
|
||||
}
|
78
src/adminserver/handlers/handler.go
Normal file
78
src/adminserver/handlers/handler.go
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
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 handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
gorilla_handlers "github.com/gorilla/handlers"
|
||||
"github.com/vmware/harbor/src/adminserver/auth"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// NewHandler returns a gorilla router which is wrapped by authenticate handler
|
||||
// and logging handler
|
||||
func NewHandler() http.Handler {
|
||||
h := newRouter()
|
||||
secrets := map[string]string{
|
||||
"uiSecret": os.Getenv("UI_SECRET"),
|
||||
"jobserviceSecret": os.Getenv("JOBSERVICE_SECRET"),
|
||||
}
|
||||
h = newAuthHandler(auth.NewSecretAuthenticator(secrets), h)
|
||||
h = gorilla_handlers.LoggingHandler(os.Stdout, h)
|
||||
return h
|
||||
}
|
||||
|
||||
type authHandler struct {
|
||||
authenticator auth.Authenticator
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func newAuthHandler(authenticator auth.Authenticator, handler http.Handler) http.Handler {
|
||||
return &authHandler{
|
||||
authenticator: authenticator,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if a.authenticator == nil {
|
||||
if a.handler != nil {
|
||||
a.handler.ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := a.authenticator.Authenticate(r)
|
||||
if err != nil {
|
||||
log.Errorf("failed to authenticate request: %v", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError),
|
||||
http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !valid {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized),
|
||||
http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if a.handler != nil {
|
||||
a.handler.ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
73
src/adminserver/handlers/handlers_test.go
Normal file
73
src/adminserver/handlers/handlers_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
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 handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vmware/harbor/src/adminserver/auth"
|
||||
)
|
||||
|
||||
type fakeAuthenticator struct {
|
||||
authenticated bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeAuthenticator) Authenticate(req *http.Request) (bool, error) {
|
||||
return f.authenticated, f.err
|
||||
}
|
||||
|
||||
type fakeHandler struct {
|
||||
responseCode int
|
||||
}
|
||||
|
||||
func (f *fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(f.responseCode)
|
||||
}
|
||||
|
||||
func TestNewAuthHandler(t *testing.T) {
|
||||
cases := []struct {
|
||||
authenticator auth.Authenticator
|
||||
handler http.Handler
|
||||
responseCode int
|
||||
}{
|
||||
|
||||
{nil, nil, http.StatusOK},
|
||||
{&fakeAuthenticator{
|
||||
authenticated: false,
|
||||
err: nil,
|
||||
}, nil, http.StatusUnauthorized},
|
||||
{&fakeAuthenticator{
|
||||
authenticated: false,
|
||||
err: errors.New("error"),
|
||||
}, nil, http.StatusInternalServerError},
|
||||
{&fakeAuthenticator{
|
||||
authenticated: true,
|
||||
err: nil,
|
||||
}, &fakeHandler{http.StatusNotFound}, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
handler := newAuthHandler(c.authenticator, c.handler)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, nil)
|
||||
assert.Equal(t, c.responseCode, w.Code, "unexpected response code")
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@ -22,9 +22,11 @@ import (
|
||||
"github.com/vmware/harbor/src/adminserver/api"
|
||||
)
|
||||
|
||||
func newHandler() http.Handler {
|
||||
func newRouter() http.Handler {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/api/configurations", api.ListCfgs).Methods("GET")
|
||||
r.HandleFunc("/api/configurations", api.UpdateCfgs).Methods("PUT")
|
||||
r.HandleFunc("/api/configurations/reset", api.ResetCfgs).Methods("POST")
|
||||
r.HandleFunc("/api/systeminfo/capacity", api.Capacity).Methods("GET")
|
||||
return r
|
||||
}
|
@ -19,7 +19,9 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/vmware/harbor/src/adminserver/handlers"
|
||||
syscfg "github.com/vmware/harbor/src/adminserver/systemcfg"
|
||||
sysinfo "github.com/vmware/harbor/src/adminserver/systeminfo"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
@ -46,13 +48,15 @@ func main() {
|
||||
}
|
||||
log.Info("system initialization completed")
|
||||
|
||||
sysinfo.Init()
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if len(port) == 0 {
|
||||
port = "80"
|
||||
}
|
||||
server := &Server{
|
||||
Port: port,
|
||||
Handler: newHandler(),
|
||||
Handler: handlers.NewHandler(),
|
||||
}
|
||||
if err := server.Serve(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -175,6 +175,11 @@ func Init() (err error) {
|
||||
//init key provider
|
||||
initKeyProvider()
|
||||
|
||||
if os.Getenv("RESET") == "true" {
|
||||
log.Info("RESET is set, resetting system configurations...")
|
||||
return Reset()
|
||||
}
|
||||
|
||||
cfg, err := GetSystemCfg()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -332,3 +337,16 @@ func decrypt(m map[string]interface{}, keys []string, secretKey string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset clears old system configurations and reloads them
|
||||
// from environment variables
|
||||
func Reset() error {
|
||||
cfg := map[string]interface{}{}
|
||||
if err := loadFromEnv(cfg, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//sync configurations into cfg store
|
||||
log.Info("updating system configurations...")
|
||||
return UpdateSystemCfg(cfg)
|
||||
}
|
||||
|
@ -117,4 +117,21 @@ func TestSystemcfg(t *testing.T) {
|
||||
cfg[comcfg.AUTHMode], comcfg.DBAuth)
|
||||
return
|
||||
}
|
||||
|
||||
if err = Reset(); err != nil {
|
||||
t.Errorf("failed to reset system configurations: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err = GetSystemCfg()
|
||||
if err != nil {
|
||||
t.Errorf("failed to get system configurations: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg[comcfg.AUTHMode] != comcfg.DBAuth {
|
||||
t.Errorf("unexpected auth mode: %s != %s",
|
||||
cfg[comcfg.AUTHMode], comcfg.DBAuth)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
35
src/adminserver/systeminfo/imagestorage/driver.go
Normal file
35
src/adminserver/systeminfo/imagestorage/driver.go
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
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 imagestorage
|
||||
|
||||
// GlobalDriver is a global image storage driver
|
||||
var GlobalDriver Driver
|
||||
|
||||
// Capacity holds information about capaticy of image storage
|
||||
type Capacity struct {
|
||||
// total size(byte)
|
||||
Total uint64 `json:"total"`
|
||||
// available size(byte)
|
||||
Free uint64 `json:"free"`
|
||||
}
|
||||
|
||||
// Driver defines methods that an image storage driver must implement
|
||||
type Driver interface {
|
||||
// Name returns a human-readable name of the driver
|
||||
Name() string
|
||||
// Cap returns the capacity of the image storage
|
||||
Cap() (*Capacity, error)
|
||||
}
|
56
src/adminserver/systeminfo/imagestorage/filesystem/driver.go
Normal file
56
src/adminserver/systeminfo/imagestorage/filesystem/driver.go
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
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 filesystem
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
storage "github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
|
||||
)
|
||||
|
||||
const (
|
||||
driverName = "filesystem"
|
||||
)
|
||||
|
||||
type driver struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// NewDriver returns an instance of filesystem driver
|
||||
func NewDriver(path string) storage.Driver {
|
||||
return &driver{
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns a human-readable name of the fielsystem driver
|
||||
func (d *driver) Name() string {
|
||||
return driverName
|
||||
}
|
||||
|
||||
// Cap returns the capacity of the filesystem storage
|
||||
func (d *driver) Cap() (*storage.Capacity, error) {
|
||||
var stat syscall.Statfs_t
|
||||
err := syscall.Statfs(d.path, &stat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &storage.Capacity{
|
||||
Total: stat.Blocks * uint64(stat.Bsize),
|
||||
Free: stat.Bavail * uint64(stat.Bsize),
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
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 filesystem
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
path := "/tmp"
|
||||
driver := NewDriver(path)
|
||||
assert.Equal(t, driver.Name(), driverName, "unexpected driver name")
|
||||
}
|
||||
|
||||
func TestCap(t *testing.T) {
|
||||
path := "/tmp"
|
||||
driver := NewDriver(path)
|
||||
_, err := driver.Cap()
|
||||
assert.Nil(t, err, "unexpected error")
|
||||
}
|
32
src/adminserver/systeminfo/systeminfo.go
Normal file
32
src/adminserver/systeminfo/systeminfo.go
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
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 systeminfo
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage"
|
||||
"github.com/vmware/harbor/src/adminserver/systeminfo/imagestorage/filesystem"
|
||||
)
|
||||
|
||||
// Init image storage driver
|
||||
func Init() {
|
||||
path := os.Getenv("IMAGE_STORE_PATH")
|
||||
if len(path) == 0 {
|
||||
path = "/data"
|
||||
}
|
||||
imagestorage.GlobalDriver = filesystem.NewDriver(path)
|
||||
}
|
@ -136,6 +136,11 @@ func (m *Manager) Load() (map[string]interface{}, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Reset configurations
|
||||
func (m *Manager) Reset() error {
|
||||
return m.Loader.Reset()
|
||||
}
|
||||
|
||||
func getCfgExpiration(m map[string]interface{}) (int, error) {
|
||||
if m == nil {
|
||||
return 0, fmt.Errorf("can not get cfg expiration as configurations are null")
|
||||
@ -227,7 +232,7 @@ func (l *Loader) Load() ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Upload configuratons to remote server
|
||||
// Upload configurations to remote server
|
||||
func (l *Loader) Upload(b []byte) error {
|
||||
req, err := http.NewRequest("PUT", l.url+"/api/configurations", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
@ -253,6 +258,33 @@ func (l *Loader) Upload(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset sends configurations resetting command to
|
||||
// remote server
|
||||
func (l *Loader) Reset() error {
|
||||
req, err := http.NewRequest("POST", l.url+"/api/configurations/reset", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "secret",
|
||||
Value: l.secret,
|
||||
})
|
||||
|
||||
resp, err := l.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected http status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
log.Debug("configurations resetted")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parser parses configurations
|
||||
type Parser struct {
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2027-02-26T20:58:40.741161013+08:00","keys":{"54c7a86e6f03a093c432c6f31d8cfcbc8637bb4ee9223de8a68971ddb9b53b35":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYgQ4QwWAHPDTlQvTSPyEDw0aAI9n9PY0hLtkgv2nbGo/mE5Da9gFX4o1wG8CNtzRWEf8RnHL1tpmmhQkRx5Byw=="}},"756dc9faa625646ff80e26a25e05e3df88254e9be68b92b68dbfea7b4697292e":{"keytype":"ecdsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpVENDQVMrZ0F3SUJBZ0lRZFlWTnZQbGtqdHlCSjlaT3YxaDQ4VEFLQmdncWhrak9QUVFEQWpBck1Ta3cKSndZRFZRUURFeUF4TUM0eE1UY3VOQzR4TkRJdmJtOTBZWEo1TFdSbGJXOHZZblZ6ZVdKdmVEQWVGdzB4TnpBeQpNamd4TWpVNE16aGFGdzB5TnpBeU1qWXhNalU0TXpoYU1Dc3hLVEFuQmdOVkJBTVRJREV3TGpFeE55NDBMakUwCk1pOXViM1JoY25rdFpHVnRieTlpZFhONVltOTRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUKNktZRzdIeC90SHJPMUh6SkRuTSs0SmdyMFJBWWR3N0w5MVhMRTJHV0lCeUJjSTRXMktSQlMxUHY4RlQwd2V4Kwo1cHNvZGZtcTdObWFCYitUQU85ZWRhTTFNRE13RGdZRFZSMFBBUUgvQkFRREFnV2dNQk1HQTFVZEpRUU1NQW9HCkNDc0dBUVVGQndNRE1Bd0dBMVVkRXdFQi93UUNNQUF3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnT25rNENWelgKU2dacFZjSy9wa01VTWFmOUpCeGRidHgvTkNxRWJpaHJUbEFDSVFEbytudkh6azF1SURLUlc5c01ZNG5zaUtxSAprcUR4UEhaRGlZVXE0UExoOHc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}},"8230fd1fbdf1d7de675cd93ec0a64685a63f0db50a65217555f321939efe59df":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDkmdEwFUhMC+NRy3TuchNprTD8HoRUE+X5RPxevxdl3qcWIFk+26GIYYMMTqFcsmDzaoGXqixdqcJA5WaTg79A=="}},"f45d9afcbb5afd810369f5c2ef84477b75502c22867e66e5f69465f18c6ae157":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESWYg9Ix/pC2lVu1WTBQ0obYVdT+P9Xh6qMkvD1YVv0t28vxoiKYobmVwAfOzdERfXTJM9jIbZwP94Q41HQB2Zg=="}}},"roles":{"root":{"keyids":["756dc9faa625646ff80e26a25e05e3df88254e9be68b92b68dbfea7b4697292e"],"threshold":1},"snapshot":{"keyids":["8230fd1fbdf1d7de675cd93ec0a64685a63f0db50a65217555f321939efe59df"],"threshold":1},"targets":{"keyids":["f45d9afcbb5afd810369f5c2ef84477b75502c22867e66e5f69465f18c6ae157"],"threshold":1},"timestamp":{"keyids":["54c7a86e6f03a093c432c6f31d8cfcbc8637bb4ee9223de8a68971ddb9b53b35"],"threshold":1}},"version":1},"signatures":[{"keyid":"756dc9faa625646ff80e26a25e05e3df88254e9be68b92b68dbfea7b4697292e","method":"ecdsa","sig":"b1V3xDWGp0YNFde9Hgx4yipiebzZedhBaVRJSfxKsjxRmFmvNty8hvTL1D7mURZkc7FJPcsN/o3xC9AlUz/isQ=="}]}
|
||||
{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2027-03-13T19:40:40.104632513+08:00","keys":{"231efb776eb60d7a240a2b3deb3d458c32062cab05db69e62df838ac09214004":{"keytype":"ecdsa-x509","keyval":{"private":null,"public":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpakNDQVRDZ0F3SUJBZ0lSQU5WNkpKblZLUHZtQ3NqVi9TcmFtVGt3Q2dZSUtvWkl6ajBFQXdJd0t6RXAKTUNjR0ExVUVBeE1nTVRBdU1URTNMalF1TVRReUwyNXZkR0Z5ZVMxa1pXMXZMMkoxYzNsaWIzZ3dIaGNOTVRjdwpNekUxTVRFME1ETTJXaGNOTWpjd016RXpNVEUwTURNMldqQXJNU2t3SndZRFZRUURFeUF4TUM0eE1UY3VOQzR4Ck5ESXZibTkwWVhKNUxXUmxiVzh2WW5WemVXSnZlREJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUEKQkNIU0JDc0VXSjV2emVaY2NTYjJkM2FNZzBzY0JhclVGV3dxR3dTZTVEN1hQSktxTW9SYlRNeFRRbmZlc25DNAoyZkRGOGZhNHFWQVlQLytCbHRBbStseWpOVEF6TUE0R0ExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhTVUVEREFLCkJnZ3JCZ0VGQlFjREF6QU1CZ05WSFJNQkFmOEVBakFBTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUErWHZrblgKMEpxelQ0WDVqdEpFUG8xQjNpSEJRZUhMb043UnVXVE95SU85QWlFQXF6bWdmNDZKUFFkV05mOHZ1TEJlUmdJYQoxVHF6S25rV2dteVhFdmplS0ljPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="}},"2e27b6898d94d3e134f1b223f8342a7f4ea18e95e51271b6d64eb7319a08b855":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf3dmn+ATN63G1xkiDPyshhISYmzsHrGXnWDDqkmqWVwLhqN8oV1IRSQLqMeHHxDB2MWSgPzo3zcRHzTJ699wTg=="}},"73c9c8cf155f7e4a4324ced1519461ceee185ce1405984f976ae3b8a4f332e43":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElK9qIW4lHiVDx3BO+UdwsIj2hOcf4kdaJcgp4+e0oHCJuJd06m/3rP55/1HpQWZ3hGbFL4jgJD/2nHKo0hFClg=="}},"bd2c62bff4fa172fb40605fb2d8658d2b9c09e655b8ebb38f30550f5bd717eea":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuAvewzw4oXj7aWBDFMyeFen0FxZO8+wxOkPElvY8l4ODIFSWHYToggerpLMKrPKN6SkNA+toDn7ZJd61nQHRDw=="}}},"roles":{"root":{"keyids":["231efb776eb60d7a240a2b3deb3d458c32062cab05db69e62df838ac09214004"],"threshold":1},"snapshot":{"keyids":["bd2c62bff4fa172fb40605fb2d8658d2b9c09e655b8ebb38f30550f5bd717eea"],"threshold":1},"targets":{"keyids":["2e27b6898d94d3e134f1b223f8342a7f4ea18e95e51271b6d64eb7319a08b855"],"threshold":1},"timestamp":{"keyids":["73c9c8cf155f7e4a4324ced1519461ceee185ce1405984f976ae3b8a4f332e43"],"threshold":1}},"version":1},"signatures":[{"keyid":"231efb776eb60d7a240a2b3deb3d458c32062cab05db69e62df838ac09214004","method":"ecdsa","sig":"hnIYijab4LoGbz1NqdyfGDkOOczhvdFtBcxIFGPovNuGDihiQuOUhFbKD2nVrxEA+t9ZZ+vKIm7LpqYE7ULXgA=="}]}
|
@ -1 +0,0 @@
|
||||
{"signed":{"_type":"Snapshot","expires":"2020-02-28T12:58:40.793595145Z","meta":{"root":{"hashes":{"sha256":"yy0hAbWa41HhHIVfmexYbqqzSc60ZB8Vz55BdPjEYxI=","sha512":"kT7I5pFqI35onn6tghIcwSR24jVgxQG/rF+Ct4eFJBpr7J8kMKL86vcD+pjbWkh/px6oPRm89+34i45woam89w=="},"length":2429},"targets":{"hashes":{"sha256":"iBYw3fp614v3WkMfhrf2gYLgsSAcS8bSuNHy3eQAKzA=","sha512":"ZC78eLxWK4o8PZoAkDOyxN8ggBDpLKXHKZUdu558B/lYVYFmXpdzKglw/87hE/jDXbfct1sh2EBmD68ESLjG1Q=="},"length":433}},"version":1},"signatures":[{"keyid":"8230fd1fbdf1d7de675cd93ec0a64685a63f0db50a65217555f321939efe59df","method":"ecdsa","sig":"/cx8YA5vwxRckZQjUQxQ+OghKEy1R2Ha8m1oHLtEfvqzKIKNyZvNo3I9AMKMDgukz85JDKRS7zFg88jkkghleA=="}]}
|
@ -1 +0,0 @@
|
||||
{"signed":{"_type":"Targets","delegations":{"keys":{},"roles":[]},"expires":"2020-02-28T20:58:40.770351325+08:00","targets":{"1.0":{"hashes":{"sha256":"E1lggRW5RZnlZBY4usWu8d36p5u5YFfr9B68jTOs+Kc="},"length":527}},"version":2},"signatures":[{"keyid":"f45d9afcbb5afd810369f5c2ef84477b75502c22867e66e5f69465f18c6ae157","method":"ecdsa","sig":"T070LEVEi5cdA1RRt0MOeYlxl+KEAyfa8uGkD0OJI/V9OQlh12aDDu7H6qhR5qk1LmQpwTHBOtdEnjZd2bkN6w=="}]}
|
@ -1 +1 @@
|
||||
{"signed":{"_type":"Timestamp","expires":"2017-03-14T12:58:40.833773016Z","meta":{"snapshot":{"hashes":{"sha256":"fPRwHL3qeGxQnGpBdKd7JcBoHMjSbQcUnpF0TTiq8io=","sha512":"vbJK5eX8iSIjWu2nZAHLzHLuETvz6kSzCOoYyn1C86BgieMHFpZmLgEj7AKuG9svBCYc17nii3B8ROfiLvNHaQ=="},"length":683}},"version":1},"signatures":[{"keyid":"54c7a86e6f03a093c432c6f31d8cfcbc8637bb4ee9223de8a68971ddb9b53b35","method":"ecdsa","sig":"fK6IF/jcigZ2mz5kqqb9Yma97zUOGB4OQqDfxQcAskW7DhpsKIWB1l+E7m0IPFBbIrL8q9l0GjlumCNVptmauw=="}]}
|
||||
{"signed":{"_type":"Timestamp","expires":"2020-03-14T11:40:40.19936068Z","meta":{"snapshot":{"hashes":{"sha256":"YvH2+0LXsb5wx6D0thpEQnW724V8LQotNzpRGSryjUY=","sha512":"hTjtW7akumqZb0roV/n7k1LkaBnNPed5yTRlwBf9Y/QoSafgOxi2KDYYNLcuYOZwNT2raq9yilFZ80DNl0qjkw=="},"length":683}},"version":1},"signatures":[{"keyid":"73c9c8cf155f7e4a4324ced1519461ceee185ce1405984f976ae3b8a4f332e43","method":"ecdsa","sig":"nWv4Pyhjdmx0jEyIejjDqEAUQ5SlOGWUKs8xAnsEOO92DMZNa/GAYaRo9TT0kdI8rgrF+NHmCAGUXy/RRRXjdw=="}]}
|
@ -22,7 +22,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
au "github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
)
|
||||
@ -40,7 +40,7 @@ type Authorizer interface {
|
||||
type AuthorizerStore struct {
|
||||
authorizers []Authorizer
|
||||
ping *url.URL
|
||||
challenges []au.Challenge
|
||||
challenges []challenge.Challenge
|
||||
}
|
||||
|
||||
// NewAuthorizerStore ...
|
||||
|
@ -21,7 +21,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
ch "github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/vmware/harbor/src/common/utils/test"
|
||||
)
|
||||
|
||||
@ -61,7 +61,7 @@ func (s *simpleAuthorizer) Authorize(req *http.Request,
|
||||
|
||||
func TestModify(t *testing.T) {
|
||||
authorizer := &simpleAuthorizer{}
|
||||
challenge := auth.Challenge{
|
||||
challenge := ch.Challenge{
|
||||
Scheme: "bearer",
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ func TestModify(t *testing.T) {
|
||||
as := &AuthorizerStore{
|
||||
authorizers: []Authorizer{authorizer},
|
||||
ping: ping,
|
||||
challenges: []auth.Challenge{challenge},
|
||||
challenges: []ch.Challenge{challenge},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://example.com/v2/ubuntu/manifests/14.04", nil)
|
||||
|
@ -18,12 +18,12 @@ package auth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
au "github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
)
|
||||
|
||||
// ParseChallengeFromResponse ...
|
||||
func ParseChallengeFromResponse(resp *http.Response) []au.Challenge {
|
||||
challenges := au.ResponseChallenges(resp)
|
||||
func ParseChallengeFromResponse(resp *http.Response) []challenge.Challenge {
|
||||
challenges := challenge.ResponseChallenges(resp)
|
||||
|
||||
return challenges
|
||||
}
|
||||
|
@ -45,12 +45,17 @@ func TestUnMarshal(t *testing.T) {
|
||||
}
|
||||
|
||||
refs := manifest.References()
|
||||
if len(refs) != 1 {
|
||||
t.Fatalf("unexpected length of reference: %d != %d", len(refs), 1)
|
||||
if len(refs) != 2 {
|
||||
t.Fatalf("unexpected length of reference: %d != %d", len(refs), 2)
|
||||
}
|
||||
|
||||
digest := "sha256:c04b14da8d1441880ed3fe6106fb2cc6fa1c9661846ac0266b8a5ec8edf37b7c"
|
||||
digest := "sha256:c54a2cc56cbb2f04003c1cd4507e118af7c0d340fe7e2720f70976c4b75237dc"
|
||||
if refs[0].Digest.String() != digest {
|
||||
t.Errorf("unexpected digest: %s != %s", refs[0].Digest.String(), digest)
|
||||
}
|
||||
|
||||
digest = "sha256:c04b14da8d1441880ed3fe6106fb2cc6fa1c9661846ac0266b8a5ec8edf37b7c"
|
||||
if refs[1].Digest.String() != digest {
|
||||
t.Errorf("unexpected digest: %s != %s", refs[1].Digest.String(), digest)
|
||||
}
|
||||
}
|
||||
|
@ -93,5 +93,13 @@ func NewAdminserver(config map[string]interface{}) (*httptest.Server, error) {
|
||||
}),
|
||||
})
|
||||
|
||||
m = append(m, &RequestHandlerMapping{
|
||||
Method: "POST",
|
||||
Pattern: "/api/configurations/reset",
|
||||
Handler: Handler(&Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}),
|
||||
})
|
||||
|
||||
return NewServer(m...), nil
|
||||
}
|
||||
|
@ -323,12 +323,6 @@ func (m *ManifestPuller) enter() (string, error) {
|
||||
blobs = append(blobs, discriptor.Digest.String())
|
||||
}
|
||||
|
||||
// config is also need to be transferred if the schema of manifest is v2
|
||||
manifest2, ok := manifest.(*schema2.DeserializedManifest)
|
||||
if ok {
|
||||
blobs = append(blobs, manifest2.Target().Digest.String())
|
||||
}
|
||||
|
||||
m.logger.Infof("all blobs of %s:%s from %s: %v", name, tag, m.srcURL, blobs)
|
||||
|
||||
for _, blob := range blobs {
|
||||
|
@ -196,6 +196,14 @@ func (c *ConfigAPI) Put() {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset system configurations
|
||||
func (c *ConfigAPI) Reset() {
|
||||
if err := config.Reset(); err != nil {
|
||||
log.Errorf("failed to reset configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
func validateCfg(c map[string]string) (bool, error) {
|
||||
isSysErr := false
|
||||
|
||||
|
@ -68,3 +68,37 @@ func TestPutConfig(t *testing.T) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetConfig(t *testing.T) {
|
||||
fmt.Println("Testing resetting configurations")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
code, err := apiTest.ResetConfig(*admin)
|
||||
if err != nil {
|
||||
t.Errorf("failed to get configurations: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.Equal(200, code, "unexpected response code") {
|
||||
return
|
||||
}
|
||||
|
||||
code, cfgs, err := apiTest.GetConfig(*admin)
|
||||
if err != nil {
|
||||
t.Errorf("failed to get configurations: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.Equal(200, code, "unexpected response code") {
|
||||
return
|
||||
}
|
||||
|
||||
value, ok := cfgs[config.VerifyRemoteCert]
|
||||
if !ok {
|
||||
t.Errorf("%s not found", config.VerifyRemoteCert)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(value.Value.(bool), true, "unexpected value")
|
||||
}
|
||||
|
@ -16,13 +16,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/src/common/api"
|
||||
comcfg "github.com/vmware/harbor/src/common/config"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/utils/email"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
@ -54,63 +52,60 @@ func (e *EmailAPI) Prepare() {
|
||||
|
||||
// Ping tests connection and authentication with email server
|
||||
func (e *EmailAPI) Ping() {
|
||||
m := map[string]string{}
|
||||
e.DecodeJSONReq(&m)
|
||||
|
||||
settings, err := config.Email()
|
||||
if err != nil {
|
||||
e.CustomAbort(http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
host, ok := m[comcfg.EmailHost]
|
||||
if ok {
|
||||
if len(host) == 0 {
|
||||
e.CustomAbort(http.StatusBadRequest, "empty email server host")
|
||||
var host, username, password, identity string
|
||||
var port int
|
||||
var ssl bool
|
||||
body := e.Ctx.Input.CopyBody(1 << 32)
|
||||
if body == nil || len(body) == 0 {
|
||||
cfg, err := config.Email()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get email configurations: %v", err)
|
||||
e.CustomAbort(http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
settings.Host = host
|
||||
}
|
||||
host = cfg.Host
|
||||
port = cfg.Port
|
||||
username = cfg.Username
|
||||
password = cfg.Password
|
||||
identity = cfg.Identity
|
||||
ssl = cfg.SSL
|
||||
} else {
|
||||
settings := &struct {
|
||||
Host string `json:"email_host"`
|
||||
Port *int `json:"email_port"`
|
||||
Username string `json:"email_username"`
|
||||
Password *string `json:"email_password"`
|
||||
SSL bool `json:"email_ssl"`
|
||||
Identity string `json:"email_identity"`
|
||||
}{}
|
||||
e.DecodeJSONReq(&settings)
|
||||
|
||||
port, ok := m[comcfg.EmailPort]
|
||||
if ok {
|
||||
if len(port) == 0 {
|
||||
e.CustomAbort(http.StatusBadRequest, "empty email server port")
|
||||
if len(settings.Host) == 0 || settings.Port == nil {
|
||||
e.CustomAbort(http.StatusBadRequest, "empty host or port")
|
||||
}
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil || p <= 0 {
|
||||
e.CustomAbort(http.StatusBadRequest, "invalid email server port")
|
||||
|
||||
if settings.Password == nil {
|
||||
cfg, err := config.Email()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get email configurations: %v", err)
|
||||
e.CustomAbort(http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
settings.Password = &cfg.Password
|
||||
}
|
||||
settings.Port = p
|
||||
|
||||
host = settings.Host
|
||||
port = *settings.Port
|
||||
username = settings.Username
|
||||
password = *settings.Password
|
||||
identity = settings.Identity
|
||||
ssl = settings.SSL
|
||||
}
|
||||
|
||||
username, ok := m[comcfg.EmailUsername]
|
||||
if ok {
|
||||
settings.Username = username
|
||||
}
|
||||
|
||||
password, ok := m[comcfg.EmailPassword]
|
||||
if ok {
|
||||
settings.Password = password
|
||||
}
|
||||
|
||||
identity, ok := m[comcfg.EmailIdentity]
|
||||
if ok {
|
||||
settings.Identity = identity
|
||||
}
|
||||
|
||||
ssl, ok := m[comcfg.EmailSSL]
|
||||
if ok {
|
||||
if ssl != "0" && ssl != "1" {
|
||||
e.CustomAbort(http.StatusBadRequest,
|
||||
fmt.Sprintf("%s should be 0 or 1", comcfg.EmailSSL))
|
||||
}
|
||||
settings.SSL = ssl == "1"
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort(settings.Host, strconv.Itoa(settings.Port))
|
||||
if err := email.Ping(
|
||||
addr, settings.Identity, settings.Username,
|
||||
settings.Password, pingEmailTimeout, settings.SSL, false); err != nil {
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
if err := email.Ping(addr, identity, username,
|
||||
password, pingEmailTimeout, ssl, false); err != nil {
|
||||
log.Debugf("ping %s failed: %v", addr, err)
|
||||
e.CustomAbort(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
comcfg "github.com/vmware/harbor/src/common/config"
|
||||
)
|
||||
|
||||
func TestPingEmail(t *testing.T) {
|
||||
@ -38,17 +37,29 @@ func TestPingEmail(t *testing.T) {
|
||||
|
||||
assert.Equal(401, code, "the status code of ping email server with non-admin user should be 401")
|
||||
|
||||
settings := map[string]string{
|
||||
comcfg.EmailHost: "smtp.gmail.com",
|
||||
comcfg.EmailPort: "465",
|
||||
comcfg.EmailIdentity: "",
|
||||
comcfg.EmailUsername: "wrong_username",
|
||||
comcfg.EmailPassword: "wrong_password",
|
||||
comcfg.EmailSSL: "1",
|
||||
//case 2: bad request
|
||||
settings := `{
|
||||
"email_host": ""
|
||||
}`
|
||||
|
||||
code, _, err = apiTest.PingEmail(*admin, []byte(settings))
|
||||
if err != nil {
|
||||
t.Errorf("failed to test ping email server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
//case 2: secure connection with admin role
|
||||
code, body, err := apiTest.PingEmail(*admin, settings)
|
||||
assert.Equal(400, code, "the status code of ping email server should be 400")
|
||||
|
||||
//case 3: secure connection with admin role
|
||||
settings = `{
|
||||
"email_host": "smtp.gmail.com",
|
||||
"email_port": 465,
|
||||
"email_identity": "",
|
||||
"email_username": "wrong_username",
|
||||
"email_ssl": true
|
||||
}`
|
||||
|
||||
code, body, err := apiTest.PingEmail(*admin, []byte(settings))
|
||||
if err != nil {
|
||||
t.Errorf("failed to test ping email server: %v", err)
|
||||
return
|
||||
@ -60,4 +71,13 @@ func TestPingEmail(t *testing.T) {
|
||||
t.Errorf("unexpected error: %s does not contains 535", body)
|
||||
return
|
||||
}
|
||||
|
||||
//case 4: ping email server whose settings are read from config
|
||||
code, _, err = apiTest.PingEmail(*admin, nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to test ping email server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(400, code, "the status code of ping email server should be 400")
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -102,6 +103,7 @@ func init() {
|
||||
beego.Router("/api/systeminfo/getcert", &SystemInfoAPI{}, "get:GetCert")
|
||||
beego.Router("/api/ldap/ping", &LdapAPI{}, "post:Ping")
|
||||
beego.Router("/api/configurations", &ConfigAPI{})
|
||||
beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset")
|
||||
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
|
||||
|
||||
_ = updateInitPassword(1, "Harbor12345")
|
||||
@ -1029,8 +1031,16 @@ func (a testapi) PutConfig(authInfo usrInfo, cfg map[string]string) (int, error)
|
||||
return code, err
|
||||
}
|
||||
|
||||
func (a testapi) PingEmail(authInfo usrInfo, settings map[string]string) (int, string, error) {
|
||||
_sling := sling.New().Base(a.basePath).Post("/api/email/ping").BodyJSON(settings)
|
||||
func (a testapi) ResetConfig(authInfo usrInfo) (int, error) {
|
||||
_sling := sling.New().Base(a.basePath).Post("/api/configurations/reset")
|
||||
|
||||
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
|
||||
return code, err
|
||||
}
|
||||
|
||||
func (a testapi) PingEmail(authInfo usrInfo, settings []byte) (int, string, error) {
|
||||
_sling := sling.New().Base(a.basePath).Post("/api/email/ping").Body(bytes.NewReader(settings))
|
||||
|
||||
code, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
|
||||
|
@ -86,7 +86,9 @@ func (s *SearchAPI) Get() {
|
||||
log.Errorf("failed to get user's project role: %v", err)
|
||||
s.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
p.Role = roles[0].RoleID
|
||||
if len(roles) != 0 {
|
||||
p.Role = roles[0].RoleID
|
||||
}
|
||||
}
|
||||
|
||||
if p.Role == models.PROJECTADMIN {
|
||||
|
@ -44,6 +44,7 @@ type GeneralInfo struct {
|
||||
RegistryURL string `json:"registry_url"`
|
||||
ProjectCreationRestrict string `json:"project_creation_restriction"`
|
||||
SelfRegistration bool `json:"self_registration"`
|
||||
HasCARoot bool `json:"has_ca_root"`
|
||||
}
|
||||
|
||||
// validate for validating user if an admin.
|
||||
@ -88,13 +89,16 @@ func (sia *SystemInfoAPI) GetVolumeInfo() {
|
||||
func (sia *SystemInfoAPI) GetCert() {
|
||||
sia.validate()
|
||||
if sia.isAdmin {
|
||||
if _, err := os.Stat(defaultRootCert); !os.IsNotExist(err) {
|
||||
if _, err := os.Stat(defaultRootCert); err == nil {
|
||||
sia.Ctx.Output.Header("Content-Type", "application/octet-stream")
|
||||
sia.Ctx.Output.Header("Content-Disposition", "attachment; filename=ca.crt")
|
||||
http.ServeFile(sia.Ctx.ResponseWriter, sia.Ctx.Request, defaultRootCert)
|
||||
} else {
|
||||
} else if os.IsNotExist(err) {
|
||||
log.Error("No certificate found.")
|
||||
sia.CustomAbort(http.StatusNotFound, "No certificate found.")
|
||||
} else {
|
||||
log.Errorf("Unexpected error: %v", err)
|
||||
sia.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
sia.CustomAbort(http.StatusForbidden, "")
|
||||
@ -113,6 +117,7 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
|
||||
} else {
|
||||
registryURL = l[0]
|
||||
}
|
||||
_, caStatErr := os.Stat(defaultRootCert)
|
||||
info := GeneralInfo{
|
||||
AdmiralEndpoint: cfg[comcfg.AdmiralEndpoint].(string),
|
||||
WithAdmiral: config.WithAdmiral(),
|
||||
@ -121,6 +126,7 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
|
||||
ProjectCreationRestrict: cfg[comcfg.ProjectCreationRestriction].(string),
|
||||
SelfRegistration: cfg[comcfg.SelfRegistration].(bool),
|
||||
RegistryURL: registryURL,
|
||||
HasCARoot: caStatErr == nil,
|
||||
}
|
||||
sia.Data["json"] = info
|
||||
sia.ServeJSON()
|
||||
|
@ -48,6 +48,7 @@ func TestGetGeneralInfo(t *testing.T) {
|
||||
err = json.Unmarshal(body, g)
|
||||
assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err))
|
||||
assert.Equal(false, g.WithNotary, "with notary should be false")
|
||||
assert.Equal(true, g.HasCARoot, "has ca root should be true")
|
||||
}
|
||||
|
||||
func TestGetCert(t *testing.T) {
|
||||
|
@ -71,6 +71,11 @@ func Load() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset configurations
|
||||
func Reset() error {
|
||||
return mg.Reset()
|
||||
}
|
||||
|
||||
// Upload uploads all system configutations to admin server
|
||||
func Upload(cfg map[string]interface{}) error {
|
||||
b, err := json.Marshal(cfg)
|
||||
|
@ -140,4 +140,17 @@ func TestConfig(t *testing.T) {
|
||||
if extURL != "host01.com" {
|
||||
t.Errorf(`extURL should be "host01.com".`)
|
||||
}
|
||||
|
||||
// reset configurations
|
||||
if err = Reset(); err != nil {
|
||||
t.Errorf("failed to reset configurations: %v", err)
|
||||
return
|
||||
}
|
||||
mode, err = AuthMode()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get auth mode: %v", err)
|
||||
}
|
||||
if mode != "db_auth" {
|
||||
t.Errorf("unexpected mode: %s != %s", mode, "db_auth")
|
||||
}
|
||||
}
|
||||
|
@ -48,12 +48,6 @@ func init() {
|
||||
beego.Router("/reset", &CommonController{}, "post:ResetPassword")
|
||||
beego.Router("/userExists", &CommonController{}, "post:UserExists")
|
||||
beego.Router("/sendEmail", &CommonController{}, "get:SendEmail")
|
||||
beego.Router("/language", &CommonController{}, "get:SwitchLanguage")
|
||||
|
||||
beego.Router("/optional_menu", &OptionalMenuController{})
|
||||
beego.Router("/navigation_header", &NavigationHeaderController{})
|
||||
beego.Router("/navigation_detail", &NavigationDetailController{})
|
||||
beego.Router("/sign_in", &SignInController{})
|
||||
|
||||
//Init user Info
|
||||
//admin = &usrInfo{adminName, adminPwd}
|
||||
@ -71,7 +65,11 @@ func TestMain(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(w, r)
|
||||
assert.Equal(int(200), w.Code, "'/' httpStatusCode should be 200")
|
||||
<<<<<<< HEAD
|
||||
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>page_title_index</title>"), "http respond should have '<title>page_title_index</title>'")
|
||||
=======
|
||||
assert.Equal(true, strings.Contains(fmt.Sprintf("%s", w.Body), "<title>Harbor</title>"), "http respond should have '<title>Harbor</title>'")
|
||||
>>>>>>> upstream/dev
|
||||
|
||||
r, _ = http.NewRequest("POST", "/login", nil)
|
||||
w = httptest.NewRecorder()
|
||||
|
@ -90,6 +90,7 @@ func initRouters() {
|
||||
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
|
||||
beego.Router("/api/logs", &api.LogAPI{})
|
||||
beego.Router("/api/configurations", &api.ConfigAPI{})
|
||||
beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset")
|
||||
|
||||
beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo")
|
||||
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
|
||||
|
@ -1,67 +0,0 @@
|
||||
{
|
||||
"project": {
|
||||
"version": "1.0.0-beta.20-4",
|
||||
"name": "clarity-seed"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"images",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "app",
|
||||
"mobile": false,
|
||||
"styles": [
|
||||
"../node_modules/clarity-icons/clarity-icons.min.css",
|
||||
"../node_modules/clarity-ui/clarity-ui.min.css",
|
||||
"styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"../node_modules/core-js/client/shim.min.js",
|
||||
"../node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
|
||||
"../node_modules/@webcomponents/custom-elements/custom-elements.min.js",
|
||||
"../node_modules/clarity-icons/clarity-icons.min.js",
|
||||
"../node_modules/web-animations-js/web-animations.min.js"
|
||||
],
|
||||
"environments": {
|
||||
"source": "environments/environment.ts",
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"addons": [],
|
||||
"packages": [],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.config.js"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"prefixInterfaces": false,
|
||||
"inline": {
|
||||
"style": false,
|
||||
"template": false
|
||||
},
|
||||
"spec": {
|
||||
"class": false,
|
||||
"component": true,
|
||||
"directive": true,
|
||||
"module": false,
|
||||
"pipe": true,
|
||||
"service": true
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/ui_ng/favicon.ico
Normal file
BIN
src/ui_ng/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
@ -4,7 +4,7 @@
|
||||
"description": "Angular-CLI starter for a Clarity project",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"start": "ng serve --host 0.0.0.0",
|
||||
"start": "ng serve --host 0.0.0.0 --proxy-config proxy.config.json",
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"@angular/common": "^2.4.1",
|
||||
"@angular/compiler": "^2.4.1",
|
||||
"@angular/core": "^2.4.1",
|
||||
"@angular/core": "^2.4.9",
|
||||
"@angular/forms": "^2.4.1",
|
||||
"@angular/http": "^2.4.1",
|
||||
"@angular/platform-browser": "^2.4.1",
|
||||
@ -24,9 +24,9 @@
|
||||
"@ngx-translate/http-loader": "0.0.3",
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"angular2-cookie": "^1.2.6",
|
||||
"clarity-angular": "^0.8.0",
|
||||
"clarity-icons": "^0.8.0",
|
||||
"clarity-ui": "^0.8.0",
|
||||
"clarity-angular": "0.8.7",
|
||||
"clarity-icons": "0.8.7",
|
||||
"clarity-ui": "0.8.7",
|
||||
"core-js": "^2.4.1",
|
||||
"fs": "0.0.1-security",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
@ -36,11 +36,12 @@
|
||||
"zone.js": "^0.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "^1.0.0-rc.1",
|
||||
"@angular/compiler-cli": "^2.4.1",
|
||||
"@types/core-js": "^0.9.34",
|
||||
"@types/jasmine": "^2.2.30",
|
||||
"@types/node": "^6.0.42",
|
||||
"angular-cli": "^1.0.0-beta.24",
|
||||
"@angular/cli": "^1.0.0-rc.2",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"codelyzer": "~1.0.0-beta.3",
|
||||
"enhanced-resolve": "^3.0.0",
|
||||
|
@ -1,36 +1,39 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop">
|
||||
<h3 class="modal-title">{{'PROFILE.TITLE' | translate}}</h3>
|
||||
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
<div class="modal-body" style="overflow-y: hidden;">
|
||||
<form #accountSettingsFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="account_settings_username" class="col-md-4">{{'PROFILE.USER_NAME' | translate}}</label>
|
||||
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="31">
|
||||
<div class="form-group form-group-override">
|
||||
<label for="account_settings_username" class="form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label>
|
||||
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="33">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_email" class="col-md-4 required">{{'PROFILE.EMAIL' | translate}}</label>
|
||||
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="eamilInput.invalid && (eamilInput.dirty || eamilInput.touched)">
|
||||
<div class="form-group form-group-override">
|
||||
<label for="account_settings_email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label>
|
||||
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("account_settings_email")'>
|
||||
<input name="account_settings_email" type="text" #eamilInput="ngModel" [(ngModel)]="account.email"
|
||||
required
|
||||
pattern='^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="28">
|
||||
pattern='^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="30"
|
||||
(input)='handleValidation("account_settings_email", false)'
|
||||
(focusout)='handleValidation("account_settings_email", true)'>
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.EMAIL' | translate}}
|
||||
{{emailTooltip | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</label><span class="spinner spinner-inline" [hidden]="!checkProgress"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_full_name" class="col-md-4 required">{{'PROFILE.FULL_NAME' | translate}}</label>
|
||||
<label for="account_settings_full_name" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="fullNameInput.invalid && (fullNameInput.dirty || fullNameInput.touched)">
|
||||
<input type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" required maxLengthExt="20" id="account_settings_full_name" size="28">
|
||||
<div class="form-group form-group-override">
|
||||
<label for="account_settings_full_name" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label>
|
||||
<label for="account_settings_full_name" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("account_settings_full_name")'>
|
||||
<input type="text" name="account_settings_full_name" #fullNameInput="ngModel" [(ngModel)]="account.realname" required maxLengthExt="20" id="account_settings_full_name" size="30" (input)='handleValidation("account_settings_full_name", false)' (focusout)='handleValidation("account_settings_full_name", true)'>
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.FULL_NAME' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_comments" class="col-md-4">{{'PROFILE.COMMENT' | translate}}</label>
|
||||
<div class="form-group form-group-override">
|
||||
<label for="account_settings_comments" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label>
|
||||
<label for="account_settings_comments" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="commentInput.invalid && (commentInput.dirty || commentInput.touched)">
|
||||
<input type="text" #commentInput="ngModel" maxLengthExt="20" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="28">
|
||||
<input type="text" #commentInput="ngModel" maxLengthExt="20" name="account_settings_comments" [(ngModel)]="account.comment" id="account_settings_comments" size="30">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.COMMENT' | translate}}
|
||||
</span>
|
||||
@ -38,7 +41,6 @@
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
|
||||
|
@ -10,7 +10,8 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
|
||||
|
||||
@Component({
|
||||
selector: "account-settings-modal",
|
||||
templateUrl: "account-settings-modal.component.html"
|
||||
templateUrl: "account-settings-modal.component.html",
|
||||
styleUrls: ['../../common.css']
|
||||
})
|
||||
|
||||
export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
@ -19,9 +20,16 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
account: SessionUser;
|
||||
error: any = null;
|
||||
originalStaticData: SessionUser;
|
||||
private emailTooltip: string = 'TOOLTIP.EMAIL';
|
||||
private validationStateMap: any = {
|
||||
"account_settings_email": true,
|
||||
"account_settings_full_name": true
|
||||
};
|
||||
private mailAlreadyChecked = {};
|
||||
|
||||
private isOnCalling: boolean = false;
|
||||
private formValueChanged: boolean = false;
|
||||
private checkOnGoing: boolean = false;
|
||||
|
||||
accountFormRef: NgForm;
|
||||
@ViewChild("accountSettingsFrom") accountForm: NgForm;
|
||||
@ -37,6 +45,54 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
this.account = Object.assign({}, this.session.getCurrentUser());
|
||||
}
|
||||
|
||||
private getValidationState(key: string): boolean {
|
||||
return this.validationStateMap[key];
|
||||
}
|
||||
|
||||
private handleValidation(key: string, flag: boolean): void {
|
||||
if (flag) {
|
||||
//Checking
|
||||
let cont = this.accountForm.controls[key];
|
||||
if (cont) {
|
||||
this.validationStateMap[key] = cont.valid;
|
||||
//Check email existing from backend
|
||||
if (cont.valid && key === "account_settings_email") {
|
||||
if (this.formValueChanged && this.account.email != this.originalStaticData.email) {
|
||||
if (this.mailAlreadyChecked[this.account.email]) {
|
||||
this.validationStateMap[key] = !this.mailAlreadyChecked[this.account.email].result;
|
||||
if (!this.validationStateMap[key]) {
|
||||
this.emailTooltip = "TOOLTIP.EMAIL_EXISTING";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//Mail changed
|
||||
this.checkOnGoing = true;
|
||||
this.session.checkUserExisting("email", this.account.email)
|
||||
.then((res: boolean) => {
|
||||
this.checkOnGoing = false;
|
||||
this.validationStateMap[key] = !res;
|
||||
if (res) {
|
||||
this.emailTooltip = "TOOLTIP.EMAIL_EXISTING";
|
||||
}
|
||||
this.mailAlreadyChecked[this.account.email] = {
|
||||
result: res
|
||||
}; //Tag it checked
|
||||
})
|
||||
.catch(error => {
|
||||
this.checkOnGoing = false;
|
||||
this.validationStateMap[key] = false;//Not valid @ backend
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//Reset
|
||||
this.validationStateMap[key] = true;
|
||||
this.emailTooltip = "TOOLTIP.EMAIL";
|
||||
}
|
||||
}
|
||||
|
||||
private isUserDataChange(): boolean {
|
||||
if (!this.originalStaticData || !this.account) {
|
||||
return false;
|
||||
@ -56,13 +112,20 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
}
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.accountForm && this.accountForm.valid && this.error === null;
|
||||
return this.accountForm &&
|
||||
this.accountForm.valid &&
|
||||
this.error === null &&
|
||||
this.validationStateMap["account_settings_email"]; //backend check is valid as well
|
||||
}
|
||||
|
||||
public get showProgress(): boolean {
|
||||
return this.isOnCalling;
|
||||
}
|
||||
|
||||
public get checkProgress(): boolean {
|
||||
return this.checkOnGoing;
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
if (this.accountFormRef != this.accountForm) {
|
||||
this.accountFormRef = this.accountForm;
|
||||
@ -118,15 +181,15 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
this.session.updateAccountSettings(this.account)
|
||||
.then(() => {
|
||||
this.isOnCalling = false;
|
||||
this.close();
|
||||
this.opened = false;
|
||||
this.msgService.announceMessage(200, "PROFILE.SAVE_SUCCESS", AlertType.SUCCESS);
|
||||
})
|
||||
.catch(error => {
|
||||
this.isOnCalling = false;
|
||||
this.error = error;
|
||||
if(accessErrorHandler(error, this.msgService)){
|
||||
if (accessErrorHandler(error, this.msgService)) {
|
||||
this.opened = false;
|
||||
}else{
|
||||
} else {
|
||||
this.inlineAlert.showInlineError(error);
|
||||
}
|
||||
});
|
||||
|
@ -9,14 +9,17 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { SignUpComponent } from './sign-up/sign-up.component';
|
||||
import { ForgotPasswordComponent } from './password/forgot-password.component';
|
||||
import { ResetPasswordComponent } from './password/reset-password.component';
|
||||
import { SignUpPageComponent } from './sign-up/sign-up-page.component';
|
||||
|
||||
import { PasswordSettingService } from './password/password-setting.service';
|
||||
import { RepositoryModule } from '../repository/repository.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreModule,
|
||||
RouterModule,
|
||||
SharedModule
|
||||
SharedModule,
|
||||
RepositoryModule
|
||||
],
|
||||
declarations: [
|
||||
SignInComponent,
|
||||
@ -24,12 +27,14 @@ import { PasswordSettingService } from './password/password-setting.service';
|
||||
AccountSettingsModalComponent,
|
||||
SignUpComponent,
|
||||
ForgotPasswordComponent,
|
||||
ResetPasswordComponent],
|
||||
ResetPasswordComponent,
|
||||
SignUpPageComponent],
|
||||
exports: [
|
||||
SignInComponent,
|
||||
PasswordSettingComponent,
|
||||
AccountSettingsModalComponent,
|
||||
ResetPasswordComponent],
|
||||
ResetPasswordComponent,
|
||||
SignUpPageComponent],
|
||||
|
||||
providers: [PasswordSettingService]
|
||||
})
|
||||
|
@ -1,17 +1,18 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
|
||||
<h3 class="modal-title">{{'RESET_PWD.TITLE' | translate}}</h3>
|
||||
<label class="modal-title reset-modal-title-override">{{'RESET_PWD.CAPTION' | translate}}</label>
|
||||
<div class="modal-body" style="overflow-y: hidden;">
|
||||
<inline-alert class="modal-title"></inline-alert>
|
||||
<div class="modal-body" style="overflow-y: hidden; min-height: 130px;">
|
||||
<form #forgotPasswordFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="reset_pwd_email" class="col-md-4 required">{{'RESET_PWD.EMAIL' | translate}}</label>
|
||||
<div class="form-group form-group-override">
|
||||
<label for="reset_pwd_email" class="required form-group-label-override">{{'RESET_PWD.EMAIL' | translate}}</label>
|
||||
<label for="reset_pwd_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="validationState === false">
|
||||
<input name="reset_pwd_email" type="text" #eamilInput="ngModel" [(ngModel)]="email"
|
||||
required
|
||||
pattern='^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$'
|
||||
id="reset_pwd_email"
|
||||
size="36"
|
||||
size="40"
|
||||
(input)="handleValidation(true)"
|
||||
(focusout)="handleValidation(false)">
|
||||
<span class="tooltip-content">
|
||||
@ -21,8 +22,6 @@
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<inline-alert></inline-alert>
|
||||
<div style="height: 30px;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
|
||||
|
@ -8,7 +8,7 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
|
||||
@Component({
|
||||
selector: 'forgot-password',
|
||||
templateUrl: "forgot-password.component.html",
|
||||
styleUrls: ['password.component.css']
|
||||
styleUrls: ['password.component.css', '../../common.css']
|
||||
})
|
||||
export class ForgotPasswordComponent {
|
||||
opened: boolean = false;
|
||||
|
@ -1,51 +1,51 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
|
||||
<h3 class="modal-title">{{'CHANGE_PWD.TITLE' | translate}}</h3>
|
||||
<div class="modal-body" style="min-height: 250px; overflow-y: hidden;">
|
||||
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
<div class="modal-body" style="overflow-y: hidden;">
|
||||
<form #changepwdForm="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="oldPassword">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
|
||||
<div class="form-group form-group-override">
|
||||
<label for="oldPassword" class="required form-group-label-override">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
|
||||
<label for="oldPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="oldPassInput.invalid && (oldPassInput.dirty || oldPassInput.touched)">
|
||||
<input type="password" id="oldPassword" placeholder='{{"PLACEHOLDER.CURRENT_PWD" | translate}}'
|
||||
required
|
||||
name="oldPassword"
|
||||
[(ngModel)]="oldPwd"
|
||||
#oldPassInput="ngModel" size="25">
|
||||
#oldPassInput="ngModel" size="30">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.CURRENT_PWD' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
|
||||
<div class="form-group form-group-override">
|
||||
<label for="newPassword" class="required form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
|
||||
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)">
|
||||
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}'
|
||||
required
|
||||
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
|
||||
name="newPassword"
|
||||
[(ngModel)]="newPwd"
|
||||
#newPassInput="ngModel" size="25">
|
||||
#newPassInput="ngModel" size="30">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.PASSWORD' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reNewPassword">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
|
||||
<div class="form-group form-group-override">
|
||||
<label for="reNewPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
|
||||
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="(reNewPassInput.invalid && (reNewPassInput.dirty || reNewPassInput.touched)) || (!newPassInput.invalid && reNewPassInput.value != newPassInput.value)">
|
||||
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
|
||||
required
|
||||
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
|
||||
name="reNewPassword"
|
||||
[(ngModel)]="reNewPwd"
|
||||
#reNewPassInput="ngModel" size="25">
|
||||
#reNewPassInput="ngModel" size="30">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.CONFIRM_PWD' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -11,7 +11,8 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
|
||||
|
||||
@Component({
|
||||
selector: 'password-setting',
|
||||
templateUrl: "password-setting.component.html"
|
||||
templateUrl: "password-setting.component.html",
|
||||
styleUrls: ['../../common.css']
|
||||
})
|
||||
export class PasswordSettingComponent implements AfterViewChecked {
|
||||
opened: boolean = false;
|
||||
@ -117,7 +118,7 @@ export class PasswordSettingComponent implements AfterViewChecked {
|
||||
})
|
||||
.then(() => {
|
||||
this.onCalling = false;
|
||||
this.close();
|
||||
this.opened = false;
|
||||
this.msgService.announceMessage(200, "CHANGE_PWD.SAVE_SUCCESS", AlertType.SUCCESS);
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Headers, Http, RequestOptions } from '@angular/http';
|
||||
import { Headers, Http, RequestOptions, URLSearchParams } from '@angular/http';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
import { PasswordSetting } from './password-setting';
|
||||
@ -52,10 +52,18 @@ export class PasswordSettingService {
|
||||
return Promise.reject("Invalid reset uuid or password");
|
||||
}
|
||||
|
||||
return this.http.post(resetPasswordEndpoint, JSON.stringify({
|
||||
"reset_uuid": uuid,
|
||||
"password": newPassword
|
||||
}), this.options)
|
||||
let formHeaders = new Headers({
|
||||
"Content-Type": 'application/x-www-form-urlencoded'
|
||||
});
|
||||
let formOptions: RequestOptions = new RequestOptions({
|
||||
headers: formHeaders
|
||||
});
|
||||
|
||||
let body: URLSearchParams = new URLSearchParams();
|
||||
body.set("reset_uuid", uuid);
|
||||
body.set("password", newPassword);
|
||||
|
||||
return this.http.post(resetPasswordEndpoint, body.toString(), formOptions)
|
||||
.toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => {
|
||||
|
@ -1,3 +1,7 @@
|
||||
.reset-modal-title-override {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.form-group-override {
|
||||
padding-left: 130px !important;
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
<form #resetPwdForm="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="newPassword">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
|
||||
<label for="newPassword" class="form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
|
||||
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword") === false'>
|
||||
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}'
|
||||
required
|
||||
@ -22,7 +22,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reNewPassword">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
|
||||
<label for="reNewPassword" class="form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
|
||||
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("reNewPassword") === false'>
|
||||
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}'
|
||||
required
|
||||
|
@ -11,7 +11,7 @@ import { MessageService } from '../../global-message/message.service';
|
||||
@Component({
|
||||
selector: 'reset-password',
|
||||
templateUrl: "reset-password.component.html",
|
||||
styleUrls: ['password.component.css']
|
||||
styleUrls: ['password.component.css', '../../common.css']
|
||||
})
|
||||
export class ResetPasswordComponent implements OnInit{
|
||||
opened: boolean = true;
|
||||
@ -44,7 +44,9 @@ export class ResetPasswordComponent implements OnInit{
|
||||
}
|
||||
|
||||
public getValidationState(key: string): boolean {
|
||||
return this.validationState && this.validationState[key];
|
||||
return this.validationState &&
|
||||
this.validationState[key] &&
|
||||
key === 'reNewPassword'?this.samePassword():true;
|
||||
}
|
||||
|
||||
public open(): void {
|
||||
@ -76,10 +78,12 @@ export class ResetPasswordComponent implements OnInit{
|
||||
this.onGoing = true;
|
||||
this.pwdService.resetPassword(this.resetUuid, this.password)
|
||||
.then(() => {
|
||||
this.onGoing = false;
|
||||
this.resetOk = true;
|
||||
this.inlineAlert.showInlineSuccess({message:'RESET_PWD.RESET_OK'});
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
if(accessErrorHandler(error, this.msgService)){
|
||||
this.close();
|
||||
}else{
|
||||
|
@ -12,4 +12,32 @@
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.popular-repo-wrapper {
|
||||
background-color: white;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-top: 24px;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
position: relative;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.login-wrapper-override {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.repo-container {
|
||||
width: 100%;
|
||||
margin-top: -250px;
|
||||
}
|
||||
|
||||
.more-info-link {
|
||||
position: relative;
|
||||
top: 100px;
|
||||
left: 330px;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<div class="login-wrapper">
|
||||
<div class="login-wrapper login-wrapper-override">
|
||||
<form #signInForm="ngForm" class="login">
|
||||
<label class="title">
|
||||
VMware Harbor<span class="trademark">™</span>
|
||||
@ -31,9 +31,15 @@
|
||||
{{ 'SIGN_IN.INVALID_MSG' | translate }}
|
||||
</div>
|
||||
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()">{{ 'BUTTON.LOG_IN' | translate }}</button>
|
||||
<a href="javascript:void(0)" class="signup" (click)="signUp()">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
|
||||
<a href="javascript:void(0)" class="signup" (click)="signUp()" *ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://github.com/vmware/harbor" target="_blank" class="more-info-link">{{ 'BUTTON.MORE_INFO' | translate }}</a>
|
||||
</div>
|
||||
</form>
|
||||
<div id="pop_repo" class="popular-repo-wrapper">
|
||||
<top-repo class="repo-container"></top-repo>
|
||||
</div>
|
||||
</div>
|
||||
<sign-up #signupDialog></sign-up>>
|
||||
<sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up>
|
||||
<forgot-password #forgotPwdDialog></forgot-password>
|
@ -7,9 +7,13 @@ import { SessionService } from '../../shared/session.service';
|
||||
import { SignInCredential } from '../../shared/sign-in-credential';
|
||||
|
||||
import { SignUpComponent } from '../sign-up/sign-up.component';
|
||||
import { harborRootRoute } from '../../shared/shared.const';
|
||||
import { CommonRoutes } from '../../shared/shared.const';
|
||||
import { ForgotPasswordComponent } from '../password/forgot-password.component';
|
||||
|
||||
import { AppConfigService } from '../../app-config.service';
|
||||
import { AppConfig } from '../../app-config';
|
||||
import { User } from '../../user/user';
|
||||
|
||||
//Define status flags for signing in states
|
||||
export const signInStatusNormal = 0;
|
||||
export const signInStatusOnGoing = 1;
|
||||
@ -23,6 +27,7 @@ export const signInStatusError = -1;
|
||||
|
||||
export class SignInComponent implements AfterViewChecked, OnInit {
|
||||
private redirectUrl: string = "";
|
||||
private appConfig: AppConfig = new AppConfig();
|
||||
//Form reference
|
||||
signInForm: NgForm;
|
||||
@ViewChild('signInForm') currentForm: NgForm;
|
||||
@ -41,13 +46,19 @@ export class SignInComponent implements AfterViewChecked, OnInit {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private session: SessionService,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private appConfigService: AppConfigService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.appConfig = this.appConfigService.getConfig();
|
||||
this.route.queryParams
|
||||
.subscribe(params => {
|
||||
this.redirectUrl = params["redirect_url"] || "";
|
||||
let isSignUp = params["sign_up"] || "";
|
||||
if (isSignUp != "") {
|
||||
this.signUp();//Open sign up
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -65,6 +76,12 @@ export class SignInComponent implements AfterViewChecked, OnInit {
|
||||
return this.currentForm.form.valid;
|
||||
}
|
||||
|
||||
//Whether show the 'sign up' link
|
||||
public get selfSignUp(): boolean {
|
||||
return this.appConfig.auth_mode === 'db_auth'
|
||||
&& this.appConfig.self_registration;
|
||||
}
|
||||
|
||||
//General error handler
|
||||
private handleError(error) {
|
||||
//Set error status
|
||||
@ -89,6 +106,17 @@ export class SignInComponent implements AfterViewChecked, OnInit {
|
||||
|
||||
}
|
||||
|
||||
//Fill the new user info into the sign in form
|
||||
private handleUserCreation(user: User): void {
|
||||
if(user){
|
||||
this.currentForm.setValue({
|
||||
"login_username": user.username,
|
||||
"login_password": user.password
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//Implement interface
|
||||
//Watch the view change only when view is in error state
|
||||
ngAfterViewChecked() {
|
||||
@ -123,8 +151,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
|
||||
//Redirect to the right route
|
||||
if (this.redirectUrl === "") {
|
||||
//Routing to the default location
|
||||
this.router.navigateByUrl(harborRootRoute);
|
||||
}else{
|
||||
this.router.navigateByUrl(CommonRoutes.HARBOR_DEFAULT);
|
||||
} else {
|
||||
this.router.navigateByUrl(this.redirectUrl);
|
||||
}
|
||||
})
|
||||
|
@ -0,0 +1,7 @@
|
||||
<h3 class="modal-title">{{'SIGN_UP.TITLE' | translate}}</h3>
|
||||
<new-user-form isSelfRegistration="true" (valueChange)="formValueChange($event)"></new-user-form>
|
||||
<div>
|
||||
<span class="spinner spinner-inline" style="top:8px;" [hidden]="!inProgress"> </span>
|
||||
<button type="button" class="btn btn-outline" [disabled]="!canBeCancelled" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || inProgress" (click)="create()">{{ 'BUTTON.SIGN_UP' | translate }}</button>
|
||||
</div>
|
97
src/ui_ng/src/app/account/sign-up/sign-up-page.component.ts
Normal file
97
src/ui_ng/src/app/account/sign-up/sign-up-page.component.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { Component, Output, ViewChild, OnInit } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
|
||||
import { User } from '../../user/user';
|
||||
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { errorHandler } from '../../shared/shared.utils';
|
||||
import { AlertType } from '../../shared/shared.const';
|
||||
|
||||
import { MessageService } from '../../global-message/message.service';
|
||||
|
||||
@Component({
|
||||
selector: 'sign-up-page',
|
||||
templateUrl: "sign-up-page.component.html"
|
||||
})
|
||||
export class SignUpPageComponent implements OnInit {
|
||||
private error: any;
|
||||
private onGoing: boolean = false;
|
||||
private formValueChanged: boolean = false;
|
||||
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private msgService: MessageService,
|
||||
private router: Router) { }
|
||||
|
||||
@ViewChild(NewUserFormComponent)
|
||||
private newUserForm: NewUserFormComponent;
|
||||
|
||||
private getNewUser(): User {
|
||||
return this.newUserForm.getData();
|
||||
}
|
||||
|
||||
public get inProgress(): boolean {
|
||||
return this.onGoing;
|
||||
}
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.newUserForm.isValid && this.error == null;
|
||||
}
|
||||
|
||||
public get canBeCancelled(): boolean {
|
||||
return this.formValueChanged && this.newUserForm && !this.newUserForm.isEmpty();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.newUserForm.reset();//Reset form
|
||||
this.formValueChanged = false;
|
||||
}
|
||||
|
||||
formValueChange(flag: boolean): void {
|
||||
if (flag) {
|
||||
this.formValueChanged = true;
|
||||
}
|
||||
if (this.error != null) {
|
||||
this.error = null;//clear error
|
||||
}
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (this.newUserForm) {
|
||||
this.newUserForm.reset();
|
||||
}
|
||||
}
|
||||
|
||||
//Create new user
|
||||
create(): void {
|
||||
//Double confirm everything is ok
|
||||
//Form is valid
|
||||
if (!this.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
//We have new user data
|
||||
let u = this.getNewUser();
|
||||
if (!u) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Start process
|
||||
this.onGoing = true;
|
||||
|
||||
this.userService.addUser(u)
|
||||
.then(() => {
|
||||
this.onGoing = false;
|
||||
this.msgService.announceMessage(200, "", AlertType.SUCCESS);
|
||||
//Navigate to embeded sign-in
|
||||
this.router.navigate(['harbor', 'sign-in']);
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
this.error = error
|
||||
this.msgService.announceMessage(error.status | 500, "", AlertType.WARNING);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="true">
|
||||
<h3 class="modal-title">{{'SIGN_UP.TITLE' | translate}}</h3>
|
||||
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
<div class="modal-body" style="overflow-y: hidden;">
|
||||
<new-user-form isSelfRegistration="true" (valueChange)="formValueChange($event)"></new-user-form>
|
||||
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="spinner spinner-inline" style="top:8px;" [hidden]="inProgress === false"> </span>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Output, ViewChild } from '@angular/core';
|
||||
import { Component, Output, ViewChild, EventEmitter } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
|
||||
@ -9,6 +9,8 @@ import { UserService } from '../../user/user.service';
|
||||
import { errorHandler } from '../../shared/shared.utils';
|
||||
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
|
||||
|
||||
import { Modal } from 'clarity-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'sign-up',
|
||||
templateUrl: "sign-up.component.html"
|
||||
@ -20,6 +22,8 @@ export class SignUpComponent {
|
||||
private onGoing: boolean = false;
|
||||
private formValueChanged: boolean = false;
|
||||
|
||||
@Output() userCreation = new EventEmitter<User>();
|
||||
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private userService: UserService) { }
|
||||
@ -30,6 +34,9 @@ export class SignUpComponent {
|
||||
@ViewChild(InlineAlertComponent)
|
||||
private inlienAlert: InlineAlertComponent;
|
||||
|
||||
@ViewChild(Modal)
|
||||
private modal: Modal;
|
||||
|
||||
private getNewUser(): User {
|
||||
return this.newUserForm.getData();
|
||||
}
|
||||
@ -55,7 +62,7 @@ export class SignUpComponent {
|
||||
open(): void {
|
||||
this.newUserForm.reset();//Reset form
|
||||
this.formValueChanged = false;
|
||||
this.opened = true;
|
||||
this.modal.open();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
@ -74,7 +81,7 @@ export class SignUpComponent {
|
||||
}
|
||||
|
||||
confirmCancel(): void {
|
||||
this.opened = false;
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
//Create new user
|
||||
@ -97,7 +104,8 @@ export class SignUpComponent {
|
||||
this.userService.addUser(u)
|
||||
.then(() => {
|
||||
this.onGoing = false;
|
||||
this.close();
|
||||
this.modal.close();
|
||||
this.userCreation.emit(u);
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
|
83
src/ui_ng/src/app/app-config.service.ts
Normal file
83
src/ui_ng/src/app/app-config.service.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Headers, Http, RequestOptions } from '@angular/http';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
import { AppConfig } from './app-config';
|
||||
import { CookieService } from 'angular2-cookie/core';
|
||||
import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
|
||||
import { maintainUrlQueryParmas } from './shared/shared.utils';
|
||||
|
||||
export const systemInfoEndpoint = "/api/systeminfo";
|
||||
/**
|
||||
* Declare service to handle the bootstrap options
|
||||
*
|
||||
*
|
||||
* @export
|
||||
* @class GlobalSearchService
|
||||
*/
|
||||
@Injectable()
|
||||
export class AppConfigService {
|
||||
private headers = new Headers({
|
||||
"Content-Type": 'application/json'
|
||||
});
|
||||
private options = new RequestOptions({
|
||||
headers: this.headers
|
||||
});
|
||||
|
||||
//Store the application configuration
|
||||
private configurations: AppConfig = new AppConfig();
|
||||
|
||||
constructor(
|
||||
private http: Http,
|
||||
private cookie: CookieService) { }
|
||||
|
||||
public load(): Promise<AppConfig> {
|
||||
return this.http.get(systemInfoEndpoint, this.options).toPromise()
|
||||
.then(response => {
|
||||
this.configurations = response.json() as AppConfig;
|
||||
|
||||
//Read admiral endpoint from cookie if existing
|
||||
let admiralUrlFromCookie: string = this.cookie.get(CookieKeyOfAdmiral);
|
||||
if(admiralUrlFromCookie){
|
||||
//Override the endpoint from configuration file
|
||||
this.configurations.admiral_endpoint = decodeURIComponent(admiralUrlFromCookie);
|
||||
}
|
||||
|
||||
return this.configurations;
|
||||
})
|
||||
.catch(error => {
|
||||
//Catch the error
|
||||
console.error("Failed to load bootstrap options with error: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
public getConfig(): AppConfig {
|
||||
return this.configurations;
|
||||
}
|
||||
|
||||
public isIntegrationMode(): boolean {
|
||||
return this.configurations &&
|
||||
this.configurations.with_admiral &&
|
||||
this.configurations.admiral_endpoint.trim() != "";
|
||||
}
|
||||
|
||||
//Return the reconstructed admiral url
|
||||
public getAdmiralEndpoint(currentHref: string): string {
|
||||
let admiralUrl:string = this.configurations.admiral_endpoint;
|
||||
if(admiralUrl.trim() === "" || currentHref.trim() === ""){
|
||||
return "#";
|
||||
}
|
||||
|
||||
return maintainUrlQueryParmas(admiralUrl, HarborQueryParamKey, encodeURIComponent(currentHref));
|
||||
}
|
||||
|
||||
public saveAdmiralEndpoint(endpoint: string): void {
|
||||
if(!(endpoint.trim())){
|
||||
return;
|
||||
}
|
||||
|
||||
//Save back to cookie
|
||||
this.cookie.put(CookieKeyOfAdmiral, endpoint);
|
||||
this.configurations.admiral_endpoint = endpoint;
|
||||
}
|
||||
}
|
20
src/ui_ng/src/app/app-config.ts
Normal file
20
src/ui_ng/src/app/app-config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export class AppConfig {
|
||||
constructor(){
|
||||
//Set default value
|
||||
this.with_notary = false;
|
||||
this.with_admiral = false;
|
||||
this.admiral_endpoint = "";
|
||||
this.auth_mode = "db_auth";
|
||||
this.registry_url = "";
|
||||
this.project_creation_restriction = "everyone";
|
||||
this.self_registration = true;
|
||||
}
|
||||
|
||||
with_notary: boolean;
|
||||
with_admiral: boolean;
|
||||
admiral_endpoint: string;
|
||||
auth_mode: string;
|
||||
registry_url: string;
|
||||
project_creation_restriction: string;
|
||||
self_registration: boolean;
|
||||
}
|
@ -16,17 +16,14 @@ import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { Http } from '@angular/http';
|
||||
|
||||
import { SessionService } from './shared/session.service';
|
||||
import { AppConfigService } from './app-config.service';
|
||||
|
||||
export function HttpLoaderFactory(http: Http) {
|
||||
return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json');
|
||||
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
|
||||
}
|
||||
|
||||
export function initConfig(session: SessionService) {
|
||||
return () => {
|
||||
console.info("app init here");
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
export function initConfig(configService: AppConfigService) {
|
||||
return () => configService.load();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
@ -51,10 +48,12 @@ export function initConfig(session: SessionService) {
|
||||
}
|
||||
})
|
||||
],
|
||||
providers: [{
|
||||
providers: [
|
||||
AppConfigService,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initConfig,
|
||||
deps: [SessionService],
|
||||
deps: [AppConfigService],
|
||||
multi: true
|
||||
}],
|
||||
bootstrap: [AppComponent]
|
||||
|
@ -5,7 +5,6 @@ import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { SearchTriggerService } from './search-trigger.service';
|
||||
import { harborRootRoute } from '../../shared/shared.const';
|
||||
|
||||
import 'rxjs/add/operator/debounceTime';
|
||||
import 'rxjs/add/operator/distinctUntilChanged';
|
||||
@ -35,7 +34,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this.searchSub = this.searchTerms
|
||||
.debounceTime(deBounceTime)
|
||||
.distinctUntilChanged()
|
||||
//.distinctUntilChanged()
|
||||
.subscribe(term => {
|
||||
this.searchTrigger.triggerSearch(term);
|
||||
});
|
||||
|
@ -1,14 +1,16 @@
|
||||
.search-overlay {
|
||||
display: block;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 98%;
|
||||
width: 100%;
|
||||
/*shoud be lesser than 1000 to aoivd override the popup menu*/
|
||||
z-index: 999;
|
||||
box-sizing: border-box;
|
||||
background: #fafafa;
|
||||
top: 0px;
|
||||
padding-left: 24px;
|
||||
top: 60px;
|
||||
left: 0px;
|
||||
padding-left: 36px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
@ -47,4 +49,8 @@
|
||||
position: relative;
|
||||
top: 8px;
|
||||
margin: 0px auto 0px auto;
|
||||
}
|
||||
|
||||
.search-header a:hover {
|
||||
text-decoration: none;
|
||||
}
|
@ -1,20 +1,14 @@
|
||||
<div class="search-overlay" *ngIf="state">
|
||||
<div id="placeholder1" style="height: 24px;"></div>
|
||||
<div class="search-header">
|
||||
<span class="search-title">Search results for '{{currentTerm}}'</span>
|
||||
<span class="search-close" (mouseover)="mouseAction(true)" (mouseout)="mouseAction(false)">
|
||||
<clr-icon shape="close" [class.is-highlight]="hover" size="36" (click)="close()"></clr-icon>
|
||||
</span>
|
||||
<a href="javascript:void(0)" (click)="close()">< {{'SEARCH.BACK' | translate}}</a>
|
||||
</div>
|
||||
<!-- spinner -->
|
||||
<div class="spinner spinner-lg search-spinner" [hidden]="done">Search...</div>
|
||||
<div class="spinner spinner-lg search-spinner" [hidden]="done">{{'SEARCH.IN_PROGRESS' | translate}}</div>
|
||||
<div id="results">
|
||||
<h2>Projects</h2>
|
||||
<div class="grid-header-wrapper">
|
||||
<grid-filter class="grid-filter" filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilterProjects($event)"></grid-filter>
|
||||
</div>
|
||||
<h2>{{'PROJECT.PROJECTS' | translate}}</h2>
|
||||
<list-project [projects]="searchResults.project" [mode]="listMode"></list-project>
|
||||
<h2>Repositories</h2>
|
||||
<h2>{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</h2>
|
||||
<list-repository [repositories]="searchResults.repository" [mode]="listMode"></list-repository>
|
||||
</div>
|
||||
</div>
|
@ -35,10 +35,6 @@ export class SearchResultComponent {
|
||||
private msgService: MessageService,
|
||||
private searchTrigger: SearchTriggerService) { }
|
||||
|
||||
private doFilterProjects(event: string) {
|
||||
this.searchResults.project = this.originalCopy.project.filter(pro => pro.name.indexOf(event) != -1);
|
||||
}
|
||||
|
||||
private clone(src: SearchResults): SearchResults {
|
||||
let res: SearchResults = new SearchResults();
|
||||
|
||||
|
@ -7,7 +7,6 @@
|
||||
}
|
||||
|
||||
.start-content-padding {
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
padding-left: 0px !important;
|
||||
padding: 0px !important;
|
||||
background-color: white;
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
<global-message [isAppLevel]="true"></global-message>
|
||||
<navigator (showAccountSettingsModal)="openModal($event)" (showPwdChangeModal)="openModal($event)"></navigator>
|
||||
<div class="content-container">
|
||||
<div class="content-area" [class.container-override]="showSearch" [class.start-content-padding]="isStartPage">
|
||||
<div class="content-area" [class.container-override]="showSearch" [class.start-content-padding]="shouldOverrideContent">
|
||||
<global-message [isAppLevel]="false"></global-message>
|
||||
<!-- Only appear when searching -->
|
||||
<search-result></search-result>
|
||||
@ -10,10 +10,9 @@
|
||||
</div>
|
||||
<nav class="sidenav" *ngIf="isUserExisting" [class.side-nav-override]="showSearch" (click)='watchClickEvt()'>
|
||||
<section class="sidenav-content">
|
||||
<a routerLink="/harbor/dashboard" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.DASHBOARD' | translate}}</a>
|
||||
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.PROJECTS' | translate}}</a>
|
||||
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link">{{'SIDE_NAV.LOGS' | translate}}</a>
|
||||
<section class="nav-group collapsible" *ngIf="isSystemAdmin">
|
||||
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link" style="margin-top: 4px;">{{'SIDE_NAV.LOGS' | translate}}</a>
|
||||
<section class="nav-group collapsible" *ngIf="isSystemAdmin" style="margin-top: 4px;">
|
||||
<input id="tabsystem" type="checkbox">
|
||||
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label>
|
||||
<ul class="nav-list">
|
||||
@ -28,5 +27,5 @@
|
||||
</clr-main-container>
|
||||
<account-settings-modal></account-settings-modal>
|
||||
<password-setting></password-setting>
|
||||
<deletion-dialog></deletion-dialog>
|
||||
<confiramtion-dialog></confiramtion-dialog>
|
||||
<about-dialog></about-dialog>
|
@ -17,7 +17,7 @@ import { SearchTriggerService } from '../global-search/search-trigger.service';
|
||||
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { harborRootRoute } from '../../shared/shared.const';
|
||||
import { CommonRoutes } from '../../shared/shared.const';
|
||||
|
||||
@Component({
|
||||
selector: 'harbor-shell',
|
||||
@ -82,8 +82,8 @@ export class HarborShellComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public get isStartPage(): boolean {
|
||||
return this.router.routerState.snapshot.url.toString() === harborRootRoute;
|
||||
public get shouldOverrideContent(): boolean {
|
||||
return this.router.routerState.snapshot.url.toString().startsWith(CommonRoutes.EMBEDDED_SIGN_IN);
|
||||
}
|
||||
|
||||
public get showSearch(): boolean {
|
||||
|
@ -17,4 +17,15 @@
|
||||
|
||||
.lang-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-divider {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background-color: #fafafa;
|
||||
position: relative;
|
||||
top: 10px;
|
||||
opacity: 0.15;
|
||||
content: '';
|
||||
}
|
@ -5,12 +5,16 @@
|
||||
<span class="title">Harbor</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-nav">
|
||||
<a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a>
|
||||
<a href="javascript:void(0)" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a>
|
||||
</div>
|
||||
<global-search></global-search>
|
||||
<div class="header-actions">
|
||||
<clr-dropdown class="dropdown bottom-left">
|
||||
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
|
||||
<button class="nav-icon" clrDropdownToggle style="width: 98px;">
|
||||
<clr-icon shape="world" style="left:-8px;"></clr-icon>
|
||||
<span>{{currentLang}}</span>
|
||||
<span style="padding-right: 8px;">{{currentLang}}</span>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Component, Output, EventEmitter, OnInit, Inject } from '@angular/core';
|
||||
import { Router, NavigationExtras } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ModalEvent } from '../modal-event';
|
||||
@ -9,7 +9,9 @@ import { SessionUser } from '../../shared/session-user';
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
import { CookieService } from 'angular2-cookie/core';
|
||||
|
||||
import { supportedLangs, enLang, languageNames } from '../../shared/shared.const';
|
||||
import { supportedLangs, enLang, languageNames, CommonRoutes } from '../../shared/shared.const';
|
||||
|
||||
import { AppConfigService } from '../../app-config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'navigator',
|
||||
@ -22,17 +24,16 @@ export class NavigatorComponent implements OnInit {
|
||||
@Output() showAccountSettingsModal = new EventEmitter<ModalEvent>();
|
||||
@Output() showPwdChangeModal = new EventEmitter<ModalEvent>();
|
||||
|
||||
private sessionUser: SessionUser = null;
|
||||
private selectedLang: string = enLang;
|
||||
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private router: Router,
|
||||
private translate: TranslateService,
|
||||
private cookie: CookieService) { }
|
||||
private cookie: CookieService,
|
||||
private appConfigService: AppConfigService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sessionUser = this.session.getCurrentUser();
|
||||
this.selectedLang = this.translate.currentLang;
|
||||
this.translate.onLangChange.subscribe(langChange => {
|
||||
this.selectedLang = langChange.lang;
|
||||
@ -42,17 +43,25 @@ export class NavigatorComponent implements OnInit {
|
||||
}
|
||||
|
||||
public get isSessionValid(): boolean {
|
||||
return this.sessionUser != null;
|
||||
return this.session.getCurrentUser() != null;
|
||||
}
|
||||
|
||||
public get accountName(): string {
|
||||
return this.sessionUser ? this.sessionUser.username : "";
|
||||
return this.session.getCurrentUser() ? this.session.getCurrentUser().username : "N/A";
|
||||
}
|
||||
|
||||
public get currentLang(): string {
|
||||
return languageNames[this.selectedLang];
|
||||
}
|
||||
|
||||
public get admiralLink(): string {
|
||||
return this.appConfigService.getAdmiralEndpoint(window.location.href);
|
||||
}
|
||||
|
||||
public get isIntegrationMode(): boolean {
|
||||
return this.appConfigService.isIntegrationMode();
|
||||
}
|
||||
|
||||
matchLang(lang: string): boolean {
|
||||
return lang.trim() === this.selectedLang;
|
||||
}
|
||||
@ -85,21 +94,22 @@ export class NavigatorComponent implements OnInit {
|
||||
logOut(): void {
|
||||
this.session.signOff()
|
||||
.then(() => {
|
||||
this.sessionUser = null;
|
||||
//Naviagte to the sign in route
|
||||
this.router.navigate(["/sign-in"]);
|
||||
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]);
|
||||
})
|
||||
.catch()//TODO:
|
||||
.catch(error => {
|
||||
console.error("Log out with error: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
//Switch languages
|
||||
switchLanguage(lang: string): void {
|
||||
if (supportedLangs.find(supportedLang => supportedLang === lang.trim())){
|
||||
if (supportedLangs.find(supportedLang => supportedLang === lang.trim())) {
|
||||
this.translate.use(lang);
|
||||
}else{
|
||||
} else {
|
||||
this.translate.use(enLang);//Use default
|
||||
//TODO:
|
||||
console.error('Language '+lang.trim()+' is not suppoted');
|
||||
console.error('Language ' + lang.trim() + ' is not suppoted');
|
||||
}
|
||||
//Try to switch backend lang
|
||||
//this.session.switchLanguage(lang).catch(error => console.error(error));
|
||||
@ -107,12 +117,12 @@ export class NavigatorComponent implements OnInit {
|
||||
|
||||
//Handle the home action
|
||||
homeAction(): void {
|
||||
if(this.sessionUser != null){
|
||||
if (this.session.getCurrentUser() != null) {
|
||||
//Navigate to default page
|
||||
this.router.navigate(['harbor']);
|
||||
}else{
|
||||
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
|
||||
} else {
|
||||
//Naviagte to signin page
|
||||
this.router.navigate(['sign-in']);
|
||||
this.router.navigate([CommonRoutes.HARBOR_ROOT]);
|
||||
}
|
||||
}
|
||||
}
|
8
src/ui_ng/src/app/common.css
Normal file
8
src/ui_ng/src/app/common.css
Normal file
@ -0,0 +1,8 @@
|
||||
.form-group-override {
|
||||
padding-left: 170px !important;
|
||||
}
|
||||
|
||||
.form-group-label-override {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
@ -3,10 +3,9 @@
|
||||
<div class="form-group">
|
||||
<label for="authMode">{{'CONFIG.AUTH_MODE' | translate }}</label>
|
||||
<div class="select">
|
||||
<select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode)
|
||||
" [(ngModel)]="currentConfig.auth_mode.value">
|
||||
<select id="authMode" name="authMode" [disabled]="disabled(currentConfig.auth_mode)" [(ngModel)]="currentConfig.auth_mode.value">
|
||||
<option value="db_auth">{{'CONFIG.AUTH_MODE_DB' | translate }}</option>
|
||||
<option value="ldap">{{'CONFIG.AUTH_MODE_LDAP' | translate }}</option>
|
||||
<option value="ldap_auth">{{'CONFIG.AUTH_MODE_LDAP' | translate }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
|
@ -20,7 +20,7 @@ export class ConfigurationAuthComponent {
|
||||
public get showLdap(): boolean {
|
||||
return this.currentConfig &&
|
||||
this.currentConfig.auth_mode &&
|
||||
this.currentConfig.auth_mode.value === 'ldap';
|
||||
this.currentConfig.auth_mode.value === 'ldap_auth';
|
||||
}
|
||||
|
||||
private disabled(prop: any): boolean {
|
||||
|
@ -0,0 +1,7 @@
|
||||
.custom-h2 {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.config-container {
|
||||
margin-left: 24px;
|
||||
}
|
@ -1,38 +1,39 @@
|
||||
<h1 style="display: inline-block;">{{'CONFIG.TITLE' | translate }}</h1>
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
|
||||
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]="true">{{'CONFIG.AUTH' | translate }}</clr-tab-link>
|
||||
<clr-tab-link [clrTabLinkId]="'config-replication'">{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
|
||||
<clr-tab-link [clrTabLinkId]="'config-email'">{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
|
||||
<clr-tab-link [clrTabLinkId]="'config-system'">{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
|
||||
<div class="config-container">
|
||||
<h2 style="display: inline-block;" class="custom-h2">{{'CONFIG.TITLE' | translate }}</h2>
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
<clr-tabs (clrTabsCurrentTabLinkChanged)="tabLinkChanged($event)">
|
||||
<clr-tab-link [clrTabLinkId]="'config-auth'" [clrTabLinkActive]="true">{{'CONFIG.AUTH' | translate }}</clr-tab-link>
|
||||
<clr-tab-link [clrTabLinkId]="'config-replication'">{{'CONFIG.REPLICATION' | translate }}</clr-tab-link>
|
||||
<clr-tab-link [clrTabLinkId]="'config-email'">{{'CONFIG.EMAIL' | translate }}</clr-tab-link>
|
||||
<clr-tab-link [clrTabLinkId]="'config-system'">{{'CONFIG.SYSTEM' | translate }}</clr-tab-link>
|
||||
|
||||
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]="true">
|
||||
<config-auth [ldapConfig]="allConfig"></config-auth>
|
||||
</clr-tab-content>
|
||||
<clr-tab-content [clrTabContentId]="'replication'">
|
||||
<form #repoConfigFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
|
||||
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
|
||||
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
|
||||
</a>
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</clr-tab-content>
|
||||
<clr-tab-content [clrTabContentId]="'email'">
|
||||
<config-email [mailConfig]="allConfig"></config-email>
|
||||
</clr-tab-content>
|
||||
<clr-tab-content [clrTabContentId]="'system_settings'">
|
||||
<form #systemConfigFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
|
||||
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
|
||||
<clr-tab-content [clrTabContentId]="'authentication'" [clrTabContentActive]="true">
|
||||
<config-auth [ldapConfig]="allConfig"></config-auth>
|
||||
</clr-tab-content>
|
||||
<clr-tab-content [clrTabContentId]="'replication'">
|
||||
<form #repoConfigFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
|
||||
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="allConfig.verify_remote_cert.value" [disabled]="disabled(allConfig.verify_remote_cert)">
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-lg tooltip-top-right" style="top:-8px;">
|
||||
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate }}</span>
|
||||
</a>
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</clr-tab-content>
|
||||
<clr-tab-content [clrTabContentId]="'email'">
|
||||
<config-email [mailConfig]="allConfig"></config-email>
|
||||
</clr-tab-content>
|
||||
<clr-tab-content [clrTabContentId]="'system_settings'">
|
||||
<form #systemConfigFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
|
||||
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
|
||||
<input name="tokenExpiration" type="text" #tokenExpirationInput="ngModel" [(ngModel)]="allConfig.token_expiration.value"
|
||||
required
|
||||
pattern="^[1-9]{1}[\d]*$"
|
||||
@ -42,17 +43,20 @@
|
||||
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</clr-tab-content>
|
||||
</clr-tabs>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</clr-tab-content>
|
||||
</clr-tabs>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
|
||||
<span class="spinner spinner-inline" [hidden]="!testingInProgress"></span>
|
||||
</div>
|
||||
</div>
|
@ -5,16 +5,18 @@ import { NgForm } from '@angular/forms';
|
||||
import { ConfigurationService } from './config.service';
|
||||
import { Configuration } from './config';
|
||||
import { MessageService } from '../global-message/message.service';
|
||||
import { AlertType, DeletionTargets } from '../shared/shared.const';
|
||||
import { AlertType, ConfirmationTargets, ConfirmationState } from '../shared/shared.const';
|
||||
import { errorHandler, accessErrorHandler } from '../shared/shared.utils';
|
||||
import { StringValueItem } from './config';
|
||||
import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog.service';
|
||||
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message'
|
||||
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message'
|
||||
|
||||
import { ConfigurationAuthComponent } from './auth/config-auth.component';
|
||||
import { ConfigurationEmailComponent } from './email/config-email.component';
|
||||
|
||||
import { AppConfigService } from '../app-config.service';
|
||||
|
||||
const fakePass = "fakepassword";
|
||||
|
||||
@Component({
|
||||
@ -28,6 +30,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
private currentTabId: string = "";
|
||||
private originalCopy: Configuration;
|
||||
private confirmSub: Subscription;
|
||||
private testingOnGoing: boolean = false;
|
||||
|
||||
@ViewChild("repoConfigFrom") repoConfigForm: NgForm;
|
||||
@ViewChild("systemConfigFrom") systemConfigForm: NgForm;
|
||||
@ -37,14 +40,19 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private msgService: MessageService,
|
||||
private configService: ConfigurationService,
|
||||
private confirmService: DeletionDialogService) { }
|
||||
private confirmService: ConfirmationDialogService,
|
||||
private appConfigService: AppConfigService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
//First load
|
||||
this.retrieveConfig();
|
||||
|
||||
this.confirmSub = this.confirmService.deletionConfirm$.subscribe(confirmation => {
|
||||
this.reset(confirmation.data);
|
||||
this.confirmSub = this.confirmService.confirmationConfirm$.subscribe(confirmation => {
|
||||
if (confirmation &&
|
||||
confirmation.state === ConfirmationState.CONFIRMED &&
|
||||
confirmation.source === ConfirmationTargets.CONFIG) {
|
||||
this.reset(confirmation.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -58,6 +66,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
return this.onGoing;
|
||||
}
|
||||
|
||||
public get testingInProgress(): boolean {
|
||||
return this.testingOnGoing;
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this.repoConfigForm &&
|
||||
this.repoConfigForm.valid &&
|
||||
@ -82,6 +94,16 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
return this.currentTabId === 'config-email';
|
||||
}
|
||||
|
||||
public get showLdapServerBtn(): boolean {
|
||||
return this.currentTabId === 'config-auth' &&
|
||||
this.allConfig.auth_mode &&
|
||||
this.allConfig.auth_mode.value === "ldap_auth";
|
||||
}
|
||||
|
||||
public isLDAPConfigValid(): boolean {
|
||||
return this.authConfig && this.authConfig.isValid();
|
||||
}
|
||||
|
||||
public tabLinkChanged(tabLink: any) {
|
||||
this.currentTabId = tabLink.id;
|
||||
}
|
||||
@ -105,6 +127,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
//or force refresh by calling service.
|
||||
//HERE we choose force way
|
||||
this.retrieveConfig();
|
||||
|
||||
//Reload bootstrap option
|
||||
this.appConfigService.load().catch(error => console.error("Failed to reload bootstrap option with error: ", error));
|
||||
|
||||
this.msgService.announceMessage(response.status, "CONFIG.SAVE_SUCCESS", AlertType.SUCCESS);
|
||||
})
|
||||
.catch(error => {
|
||||
@ -128,12 +154,12 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
public cancel(): void {
|
||||
let changes = this.getChanges();
|
||||
if (!this.isEmpty(changes)) {
|
||||
let msg = new DeletionMessage(
|
||||
let msg = new ConfirmationMessage(
|
||||
"CONFIG.CONFIRM_TITLE",
|
||||
"CONFIG.CONFIRM_SUMMARY",
|
||||
"",
|
||||
changes,
|
||||
DeletionTargets.EMPTY
|
||||
ConfirmationTargets.CONFIG
|
||||
);
|
||||
this.confirmService.openComfirmDialog(msg);
|
||||
} else {
|
||||
@ -150,7 +176,46 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
* @memberOf ConfigurationComponent
|
||||
*/
|
||||
public testMailServer(): void {
|
||||
let mailSettings = {};
|
||||
let allChanges = this.getChanges();
|
||||
for (let prop in allChanges) {
|
||||
if (prop.startsWith("email_")) {
|
||||
mailSettings[prop] = allChanges[prop];
|
||||
}
|
||||
}
|
||||
|
||||
this.testingOnGoing = true;
|
||||
this.configService.testMailServer(mailSettings)
|
||||
.then(response => {
|
||||
this.testingOnGoing = false;
|
||||
this.msgService.announceMessage(200, "CONFIG.TEST_MAIL_SUCCESS", AlertType.SUCCESS);
|
||||
})
|
||||
.catch(error => {
|
||||
this.testingOnGoing = false;
|
||||
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
|
||||
});
|
||||
}
|
||||
|
||||
public testLDAPServer(): void {
|
||||
let ldapSettings = {};
|
||||
let allChanges = this.getChanges();
|
||||
for (let prop in allChanges) {
|
||||
if (prop.startsWith("ldap_")) {
|
||||
ldapSettings[prop] = allChanges[prop];
|
||||
}
|
||||
}
|
||||
|
||||
console.info(ldapSettings);
|
||||
this.testingOnGoing = true;
|
||||
this.configService.testLDAPServer(ldapSettings)
|
||||
.then(respone => {
|
||||
this.testingOnGoing = false;
|
||||
this.msgService.announceMessage(200, "CONFIG.TEST_LDAP_SUCCESS", AlertType.SUCCESS);
|
||||
})
|
||||
.catch(error => {
|
||||
this.testingOnGoing = false;
|
||||
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.WARNING);
|
||||
});
|
||||
}
|
||||
|
||||
private retrieveConfig(): void {
|
||||
|
@ -5,6 +5,8 @@ import 'rxjs/add/operator/toPromise';
|
||||
import { Configuration } from './config';
|
||||
|
||||
const configEndpoint = "/api/configurations";
|
||||
const emailEndpoint = "/api/email/ping";
|
||||
const ldapEndpoint = "/api/ldap/ping";
|
||||
|
||||
@Injectable()
|
||||
export class ConfigurationService {
|
||||
@ -30,4 +32,18 @@ export class ConfigurationService {
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public testMailServer(mailSettings: any): Promise<any> {
|
||||
return this.http.post(emailEndpoint, JSON.stringify(mailSettings), this.options)
|
||||
.toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public testLDAPServer(ldapSettings: any): Promise<any> {
|
||||
return this.http.post(ldapEndpoint, JSON.stringify(ldapSettings), this.options)
|
||||
.toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,6 @@
|
||||
<label for="emailUsername">{{'CONFIG.MAIL_USERNAME' | translate}}</label>
|
||||
<label for="emailUsername" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
|
||||
<input name="emailUsername" type="text" #emailUsernameInput="ngModel" [(ngModel)]="currentConfig.email_username.value"
|
||||
required
|
||||
id="emailUsername"
|
||||
size="40" [disabled]="disabled(currentConfig.email_username)">
|
||||
<span class="tooltip-content">
|
||||
@ -41,7 +40,6 @@
|
||||
<label for="emailPassword">{{'CONFIG.MAIL_PASSWORD' | translate}}</label>
|
||||
<label for="emailPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="false">
|
||||
<input name="emailPassword" type="password" #emailPasswordInput="ngModel" [(ngModel)]="currentConfig.email_password.value"
|
||||
required
|
||||
id="emailPassword"
|
||||
size="40" [disabled]="disabled(currentConfig.email_password)">
|
||||
<span class="tooltip-content">
|
||||
|
@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { Message } from './message';
|
||||
import { MessageService } from './message.service';
|
||||
|
||||
import { AlertType, dismissInterval, httpStatusCode } from '../shared/shared.const';
|
||||
import { AlertType, dismissInterval, httpStatusCode, CommonRoutes } from '../shared/shared.const';
|
||||
|
||||
@Component({
|
||||
selector: 'global-message',
|
||||
@ -94,7 +94,7 @@ export class MessageComponent implements OnInit {
|
||||
}
|
||||
|
||||
signIn(): void {
|
||||
this.router.navigate(['sign-in']);
|
||||
this.router.navigateByUrl(CommonRoutes.EMBEDDED_SIGN_IN);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
|
@ -19,7 +19,6 @@ import { ReplicationComponent } from './replication/replication.component';
|
||||
import { MemberComponent } from './project/member/member.component';
|
||||
import { AuditLogComponent } from './log/audit-log.component';
|
||||
|
||||
import { BaseRoutingResolver } from './shared/route/base-routing-resolver.service';
|
||||
import { ProjectRoutingResolver } from './project/project-routing-resolver.service';
|
||||
import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
|
||||
import { SignUpComponent } from './account/sign-up/sign-up.component';
|
||||
@ -28,25 +27,22 @@ import { RecentLogComponent } from './log/recent-log.component';
|
||||
import { ConfigurationComponent } from './config/config.component';
|
||||
import { PageNotFoundComponent } from './shared/not-found/not-found.component'
|
||||
import { StartPageComponent } from './base/start-page/start.component';
|
||||
import { SignUpPageComponent } from './account/sign-up/sign-up-page.component';
|
||||
|
||||
import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
|
||||
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
|
||||
import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deactivate.service';
|
||||
|
||||
const harborRoutes: Routes = [
|
||||
{ path: '', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
|
||||
{ path: 'harbor', redirectTo: '/harbor/dashboard', pathMatch: 'full' },
|
||||
{ path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
|
||||
{ path: 'sign-up', component: SignUpComponent},
|
||||
{ path: 'reset_password', component: ResetPasswordComponent},
|
||||
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
|
||||
{ path: 'password-reset', component: ResetPasswordComponent },
|
||||
{
|
||||
path: 'harbor',
|
||||
component: HarborShellComponent,
|
||||
canActivateChild: [AuthCheckGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: StartPageComponent
|
||||
},
|
||||
{ path: '', redirectTo: 'sign-in', pathMatch: 'full' },
|
||||
{ path: 'sign-in', component: SignInComponent, canActivate: [SignInGuard] },
|
||||
{
|
||||
path: 'projects',
|
||||
component: ProjectComponent
|
||||
@ -109,10 +105,11 @@ const harborRoutes: Routes = [
|
||||
path: 'configs',
|
||||
component: ConfigurationComponent,
|
||||
canActivate: [SystemAdminGuard],
|
||||
canDeactivate: [LeavingConfigRouteDeactivate]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ path: "**", component: PageNotFoundComponent}
|
||||
{ path: "**", component: PageNotFoundComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
5
src/ui_ng/src/app/log/audit-log.component.css
Normal file
5
src/ui_ng/src/app/log/audit-log.component.css
Normal file
@ -0,0 +1,5 @@
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 22px;
|
||||
margin-bottom: 2px;
|
||||
}
|
@ -1,18 +1,16 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-right">
|
||||
<div class="row flex-items-xs-right option-right">
|
||||
<div class="flex-xs-middle">
|
||||
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
|
||||
</div>
|
||||
<div class="flex-xs-middle">
|
||||
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></grid-filter>
|
||||
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-items-xs-right" [hidden]="currentOption === 0">
|
||||
<div class="row flex-items-xs-right option-right" [hidden]="currentOption === 0">
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
|
||||
<button class="btn btn-link" clrDropdownToggle>
|
||||
{{'AUDIT_LOG.ALL_OPERATIONS' | translate}}
|
||||
{{'AUDIT_LOG.OPERATIONS' | translate}}
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@ -24,6 +22,8 @@
|
||||
<clr-icon shape="date"></clr-icon><input type="date" #toTime (change)="doSearchByTimeRange(toTime.value, 'end')">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 datagrid-margin-top ">
|
||||
<clr-datagrid (clrDgRefresh)="retrieve($event)">
|
||||
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
|
||||
|
@ -30,9 +30,10 @@ class FilterOption {
|
||||
}
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'audit-log',
|
||||
templateUrl: './audit-log.component.html',
|
||||
styleUrls: [ 'audit-log.css' ]
|
||||
styleUrls: [ './audit-log.component.css' ]
|
||||
})
|
||||
export class AuditLogComponent implements OnInit {
|
||||
|
||||
@ -53,7 +54,7 @@ export class AuditLogComponent implements OnInit {
|
||||
];
|
||||
|
||||
pageOffset: number = 1;
|
||||
pageSize: number = 2;
|
||||
pageSize: number = 15;
|
||||
totalRecordCount: number;
|
||||
totalPage: number;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user