mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-22 16:48:30 +01:00
Merge remote-tracking branch 'upstream/dev' into dev-revised
This commit is contained in:
commit
f2a1659d96
@ -517,7 +517,6 @@ paths:
|
||||
responses:
|
||||
200:
|
||||
description: Get current user information successfully.
|
||||
in: body
|
||||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
401:
|
||||
@ -659,6 +658,11 @@ paths:
|
||||
format: int32
|
||||
required: true
|
||||
description: Relevant project ID.
|
||||
- name: detail
|
||||
in: query
|
||||
type: boolean
|
||||
required: false
|
||||
description: Get detail info or not.
|
||||
- name: q
|
||||
in: query
|
||||
type: string
|
||||
@ -1475,6 +1479,31 @@ paths:
|
||||
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.
|
||||
description: |
|
||||
Test connection and authentication with email server.
|
||||
parameters:
|
||||
- name: settings
|
||||
in: body
|
||||
description: Email server settings, if some of the settings are not assigned, they will be read from system configuration.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/definitions/EmailServerSetting'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
200:
|
||||
description: Ping email server successfully.
|
||||
400:
|
||||
description: Inviald email server settings.
|
||||
401:
|
||||
description: User need to login first.
|
||||
403:
|
||||
description: Only admin has this authority.
|
||||
500:
|
||||
description: Unexpected internal errors.
|
||||
definitions:
|
||||
Search:
|
||||
type: object
|
||||
@ -1483,26 +1512,12 @@ definitions:
|
||||
description: Search results of the projects that matched the filter keywords.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/SearchProject'
|
||||
$ref: '#/definitions/Project'
|
||||
repositories:
|
||||
description: Search results of the repositories that matched the filter keywords.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/SearchRepository'
|
||||
SearchProject:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The ID of project
|
||||
name:
|
||||
type: string
|
||||
description: The name of the project
|
||||
public:
|
||||
type: integer
|
||||
format: int
|
||||
description: The flag to indicate the publicity of the project (1 is public, 0 is non-public)
|
||||
SearchRepository:
|
||||
type: object
|
||||
properties:
|
||||
@ -1518,6 +1533,12 @@ definitions:
|
||||
repository_name:
|
||||
type: string
|
||||
description: The name of the repository
|
||||
pull_count:
|
||||
type: integer
|
||||
description: The count how many times the repository is pulled
|
||||
tags_count:
|
||||
type: integer
|
||||
description: The count of tags in the repository
|
||||
ProjectReq:
|
||||
type: object
|
||||
properties:
|
||||
@ -1983,3 +2004,24 @@ definitions:
|
||||
error:
|
||||
type: string
|
||||
description: fail reason.
|
||||
EmailServerSetting:
|
||||
type: object
|
||||
properties:
|
||||
email_host:
|
||||
type: string
|
||||
description: The host of email server.
|
||||
email_port:
|
||||
type: string
|
||||
description: The port of email server.
|
||||
email_username:
|
||||
type: string
|
||||
description: The username of email server.
|
||||
email_password:
|
||||
type: string
|
||||
description: The password of email server.
|
||||
email_ssl:
|
||||
type: string
|
||||
description: Use ssl/tls or not.
|
||||
email_identity:
|
||||
type: string
|
||||
description: The dentity of email server.
|
||||
|
@ -29,7 +29,6 @@ HARBOR_ADMIN_PASSWORD=$harbor_admin_password
|
||||
PROJECT_CREATION_RESTRICTION=$project_creation_restriction
|
||||
VERIFY_REMOTE_CERT=$verify_remote_cert
|
||||
MAX_JOB_WORKERS=$max_job_workers
|
||||
LOG_DIR=/var/log/jobs
|
||||
UI_SECRET=$ui_secret
|
||||
JOBSERVICE_SECRET=$jobservice_secret
|
||||
TOKEN_EXPIRATION=$token_expiration
|
||||
|
@ -62,8 +62,8 @@ http {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ ^/v2/(.*)/_trust/(.*) {
|
||||
proxy_pass http://notary-server/v2/$$1/_trust/$$2;
|
||||
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;
|
||||
@ -74,6 +74,7 @@ http {
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /v2/ {
|
||||
proxy_pass http://registry/v2/;
|
||||
proxy_set_header Host $$http_host;
|
||||
|
@ -20,7 +20,7 @@
|
||||
"type": "token",
|
||||
"options": {
|
||||
"realm": "$token_endpoint/service/token",
|
||||
"service": "harbor-registry",
|
||||
"service": "harbor-notary",
|
||||
"issuer": "harbor-token-issuer",
|
||||
"rootcertbundle": "/config/root.crt"
|
||||
}
|
||||
|
@ -8,6 +8,43 @@ hostname = reg.mydomain.com
|
||||
#It can be set to https if ssl is enabled on nginx.
|
||||
ui_url_protocol = http
|
||||
|
||||
#The password for the root user of mysql db, change this before any production use.
|
||||
db_password = root123
|
||||
|
||||
#Determine whether the UI should use compressed js files.
|
||||
#For production, set it to on. For development, set it to off.
|
||||
use_compressed_js = on
|
||||
|
||||
#Maximum number of job workers in job service
|
||||
max_job_workers = 3
|
||||
|
||||
#Determine whether or not to generate certificate for the registry's token.
|
||||
#If the value is on, the prepare script creates new root cert and private key
|
||||
#for generating token to access the registry. If the value is off, a key/certificate must
|
||||
#be supplied for token generation.
|
||||
customize_crt = on
|
||||
|
||||
#Information of your organization for certificate
|
||||
crt_country = CN
|
||||
crt_state = State
|
||||
crt_location = CN
|
||||
crt_organization = organization
|
||||
crt_organizationalunit = organizational unit
|
||||
crt_commonname = example.com
|
||||
crt_email = example@example.com
|
||||
|
||||
#The path of cert and key files for nginx, they are applied only the protocol is set to https
|
||||
ssl_cert = /data/cert/server.crt
|
||||
ssl_cert_key = /data/cert/server.key
|
||||
|
||||
#The path of secretkey storage
|
||||
secretkey_path = /data
|
||||
|
||||
#NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES
|
||||
#only take effect in the first boot, the subsequent changes of these properties
|
||||
#should be performed on web ui
|
||||
#************************BEGIN INITIAL PROPERTIES************************
|
||||
|
||||
#Email account settings for sending out password resetting emails.
|
||||
|
||||
#Email server uses the given username and password to authenticate on TLS connections to host and act as identity.
|
||||
@ -55,50 +92,19 @@ ldap_scope = 3
|
||||
#Timeout (in seconds) when connecting to an LDAP Server. The default value (and most reasonable) is 5 seconds.
|
||||
ldap_timeout = 5
|
||||
|
||||
#The password for the root user of mysql db, change this before any production use.
|
||||
db_password = root123
|
||||
|
||||
#Turn on or off the self-registration feature
|
||||
self_registration = on
|
||||
|
||||
#Determine whether the UI should use compressed js files.
|
||||
#For production, set it to on. For development, set it to off.
|
||||
use_compressed_js = on
|
||||
|
||||
#Maximum number of job workers in job service
|
||||
max_job_workers = 3
|
||||
|
||||
#The expiration time (in minute) of token created by token service, default is 30 minutes
|
||||
token_expiration = 30
|
||||
|
||||
#Determine whether the job service should verify the ssl cert when it connects to a remote registry.
|
||||
#Set this flag to off when the remote registry uses a self-signed or untrusted certificate.
|
||||
verify_remote_cert = on
|
||||
|
||||
#Determine whether or not to generate certificate for the registry's token.
|
||||
#If the value is on, the prepare script creates new root cert and private key
|
||||
#for generating token to access the registry. If the value is off, a key/certificate must
|
||||
#be supplied for token generation.
|
||||
customize_crt = on
|
||||
|
||||
#Information of your organization for certificate
|
||||
crt_country = CN
|
||||
crt_state = State
|
||||
crt_location = CN
|
||||
crt_organization = organization
|
||||
crt_organizationalunit = organizational unit
|
||||
crt_commonname = example.com
|
||||
crt_email = example@example.com
|
||||
|
||||
#The flag to control what users have permission to create projects
|
||||
#Be default everyone can create a project, set to "adminonly" such that only admin can create project.
|
||||
project_creation_restriction = everyone
|
||||
|
||||
#The path of cert and key files for nginx, they are applied only the protocol is set to https
|
||||
ssl_cert = /data/cert/server.crt
|
||||
ssl_cert_key = /data/cert/server.key
|
||||
|
||||
#The path of secretkey storage
|
||||
secretkey_path = /data
|
||||
#Determine whether the job service should verify the ssl cert when it connects to a remote registry.
|
||||
#Set this flag to off when the remote registry uses a self-signed or untrusted certificate.
|
||||
verify_remote_cert = on
|
||||
#************************BEGIN INITIAL PROPERTIES************************
|
||||
#############
|
||||
|
||||
|
@ -46,8 +46,8 @@ var (
|
||||
comcfg.AdminInitialPassword,
|
||||
}
|
||||
|
||||
// envs are configurations need read from environment variables
|
||||
envs = map[string]interface{}{
|
||||
// all configurations need read from environment variables
|
||||
allEnvs = map[string]interface{}{
|
||||
comcfg.ExtEndpoint: "EXT_ENDPOINT",
|
||||
comcfg.AUTHMode: "AUTH_MODE",
|
||||
comcfg.SelfRegistration: &parser{
|
||||
@ -96,7 +96,6 @@ var (
|
||||
env: "TOKEN_EXPIRATION",
|
||||
parse: parseStringToInt,
|
||||
},
|
||||
comcfg.JobLogDir: "LOG_DIR",
|
||||
comcfg.UseCompressedJS: &parser{
|
||||
env: "USE_COMPRESSED_JS",
|
||||
parse: parseStringToBool,
|
||||
@ -116,6 +115,26 @@ var (
|
||||
comcfg.ProjectCreationRestriction: "PROJECT_CREATION_RESTRICTION",
|
||||
comcfg.AdminInitialPassword: "HARBOR_ADMIN_PASSWORD",
|
||||
}
|
||||
|
||||
// configurations need read from environment variables
|
||||
// every time the system startup
|
||||
repeatLoadEnvs = map[string]interface{}{
|
||||
comcfg.ExtEndpoint: "EXT_ENDPOINT",
|
||||
comcfg.MySQLPassword: "MYSQL_PWD",
|
||||
comcfg.MaxJobWorkers: &parser{
|
||||
env: "MAX_JOB_WORKERS",
|
||||
parse: parseStringToInt,
|
||||
},
|
||||
// TODO remove this config?
|
||||
comcfg.UseCompressedJS: &parser{
|
||||
env: "USE_COMPRESSED_JS",
|
||||
parse: parseStringToBool,
|
||||
},
|
||||
comcfg.CfgExpiration: &parser{
|
||||
env: "CFG_EXPIRATION",
|
||||
parse: parseStringToInt,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type parser struct {
|
||||
@ -152,16 +171,19 @@ func Init() (err error) {
|
||||
}
|
||||
|
||||
if cfg != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("configurations read from store driver are null, initializing system from environment variables...")
|
||||
cfg, err = loadFromEnv()
|
||||
if err != nil {
|
||||
return err
|
||||
if err = loadFromEnv(cfg, false); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info("configurations read from store driver are null, initializing system from environment variables...")
|
||||
cfg = make(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)
|
||||
}
|
||||
|
||||
@ -198,9 +220,13 @@ func initKeyProvider() {
|
||||
keyProvider = comcfg.NewFileKeyProvider(path)
|
||||
}
|
||||
|
||||
//load the configurations from env
|
||||
func loadFromEnv() (map[string]interface{}, error) {
|
||||
cfg := map[string]interface{}{}
|
||||
// load the configurations from allEnvs, if all is false, it just loads
|
||||
// the repeatLoadEnvs
|
||||
func loadFromEnv(cfg map[string]interface{}, all bool) error {
|
||||
envs := repeatLoadEnvs
|
||||
if all {
|
||||
envs = allEnvs
|
||||
}
|
||||
|
||||
for k, v := range envs {
|
||||
if str, ok := v.(string); ok {
|
||||
@ -211,16 +237,16 @@ func loadFromEnv() (map[string]interface{}, error) {
|
||||
if parser, ok := v.(*parser); ok {
|
||||
i, err := parser.parse(os.Getenv(parser.env))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
cfg[k] = i
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%v is not string or parse type", v)
|
||||
return fmt.Errorf("%v is not string or parse type", v)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSystemCfg returns the system configurations
|
||||
|
@ -119,6 +119,8 @@ func (b *BaseAPI) GetUserIDForRequest() (int, bool, bool) {
|
||||
user = nil
|
||||
}
|
||||
if user != nil {
|
||||
b.SetSession("userId", user.UserID)
|
||||
b.SetSession("username", user.Username)
|
||||
// User login successfully no further check required.
|
||||
return user.UserID, false, true
|
||||
}
|
||||
|
@ -168,10 +168,15 @@ func ToggleProjectPublicity(projectID int64, publicity int) error {
|
||||
// 2. the prject is public or the user is a member of the project
|
||||
func SearchProjects(userID int) ([]models.Project, error) {
|
||||
o := GetOrmer()
|
||||
sql := `select distinct p.project_id, p.name, p.public
|
||||
|
||||
sql :=
|
||||
`select distinct p.project_id, p.name, p.public,
|
||||
p.owner_id, p.creation_time, p.update_time, pm.role role
|
||||
from project p
|
||||
left join project_member pm on p.project_id = pm.project_id
|
||||
where (pm.user_id = ? or p.public = 1) and p.deleted = 0`
|
||||
left join project_member pm
|
||||
on p.project_id = pm.project_id
|
||||
where (pm.user_id = ? or p.public = 1)
|
||||
and p.deleted = 0 `
|
||||
|
||||
var projects []models.Project
|
||||
|
||||
|
@ -50,7 +50,8 @@ func GetRepositoryByName(name string) (*models.RepoRecord, error) {
|
||||
func GetAllRepositories() ([]models.RepoRecord, error) {
|
||||
o := GetOrmer()
|
||||
var repos []models.RepoRecord
|
||||
_, err := o.QueryTable("repository").All(&repos)
|
||||
_, err := o.QueryTable("repository").
|
||||
OrderBy("Name").All(&repos)
|
||||
return repos, err
|
||||
}
|
||||
|
||||
@ -183,3 +184,34 @@ func GetTotalOfUserRelevantRepositories(userID int, name string) (int64, error)
|
||||
err := GetOrmer().Raw(sql, params).QueryRow(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
// GetTotalOfRepositoriesByProject ...
|
||||
func GetTotalOfRepositoriesByProject(projectID int64, name string) (int64, error) {
|
||||
qs := GetOrmer().QueryTable(&models.RepoRecord{}).
|
||||
Filter("ProjectID", projectID)
|
||||
|
||||
if len(name) != 0 {
|
||||
qs = qs.Filter("Name__contains", name)
|
||||
}
|
||||
|
||||
return qs.Count()
|
||||
}
|
||||
|
||||
// GetRepositoriesByProject ...
|
||||
func GetRepositoriesByProject(projectID int64, name string,
|
||||
limit, offset int64) ([]*models.RepoRecord, error) {
|
||||
|
||||
repositories := []*models.RepoRecord{}
|
||||
|
||||
qs := GetOrmer().QueryTable(&models.RepoRecord{}).
|
||||
Filter("ProjectID", projectID)
|
||||
|
||||
if len(name) != 0 {
|
||||
qs = qs.Filter("Name__contains", name)
|
||||
}
|
||||
|
||||
_, err := qs.Limit(limit).
|
||||
Offset(offset).All(&repositories)
|
||||
|
||||
return repositories, err
|
||||
}
|
||||
|
@ -340,6 +340,78 @@ func TestGetTopRepos(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTotalOfRepositoriesByProject(t *testing.T) {
|
||||
var projectID int64 = 1
|
||||
repoName := "library/total_count"
|
||||
|
||||
total, err := GetTotalOfRepositoriesByProject(projectID, repoName)
|
||||
if err != nil {
|
||||
t.Errorf("failed to get total of repositoreis of project %d: %v", projectID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := addRepository(&models.RepoRecord{
|
||||
Name: repoName,
|
||||
OwnerName: "admin",
|
||||
ProjectName: "library",
|
||||
}); err != nil {
|
||||
t.Errorf("failed to add repository %s: %v", repoName, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := deleteRepository(repoName); err != nil {
|
||||
t.Errorf("failed to delete repository %s: %v", name, err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
n, err := GetTotalOfRepositoriesByProject(projectID, repoName)
|
||||
if err != nil {
|
||||
t.Errorf("failed to get total of repositoreis of project %d: %v", projectID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if n != total+1 {
|
||||
t.Errorf("unexpected total: %d != %d", n, total+1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepositoriesByProject(t *testing.T) {
|
||||
var projectID int64 = 1
|
||||
repoName := "library/repository"
|
||||
|
||||
if err := addRepository(&models.RepoRecord{
|
||||
Name: repoName,
|
||||
OwnerName: "admin",
|
||||
ProjectName: "library",
|
||||
}); err != nil {
|
||||
t.Errorf("failed to add repository %s: %v", repoName, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := deleteRepository(repoName); err != nil {
|
||||
t.Errorf("failed to delete repository %s: %v", name, err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
repositories, err := GetRepositoriesByProject(projectID, repoName, 10, 0)
|
||||
if err != nil {
|
||||
t.Errorf("failed to get repositoreis of project %d: %v", projectID, err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Log(repositories)
|
||||
|
||||
for _, repository := range repositories {
|
||||
if repository.Name == repoName {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Errorf("repository %s not found", repoName)
|
||||
}
|
||||
|
||||
func addRepository(repository *models.RepoRecord) error {
|
||||
return AddRepository(*repository)
|
||||
}
|
||||
|
@ -17,15 +17,16 @@ package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
tlspkg "crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
//"strings"
|
||||
"time"
|
||||
|
||||
"net/smtp"
|
||||
"text/template"
|
||||
|
||||
//"github.com/astaxie/beego"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
@ -37,11 +38,12 @@ type Mail struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
var mc models.Email
|
||||
var mc *models.Email
|
||||
|
||||
// SendMail sends Email according to the configurations
|
||||
func (m Mail) SendMail() error {
|
||||
mc, err := config.Email()
|
||||
var err error
|
||||
mc, err = config.Email()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -72,7 +74,7 @@ func sendMail(m Mail, auth smtp.Auth, content []byte) error {
|
||||
}
|
||||
|
||||
func sendMailWithTLS(m Mail, auth smtp.Auth, content []byte) error {
|
||||
conn, err := tls.Dial("tcp", mc.Host+":"+strconv.Itoa(mc.Port), nil)
|
||||
conn, err := tlspkg.Dial("tcp", mc.Host+":"+strconv.Itoa(mc.Port), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -117,24 +119,76 @@ func sendMailWithTLS(m Mail, auth smtp.Auth, content []byte) error {
|
||||
return client.Quit()
|
||||
}
|
||||
|
||||
/*
|
||||
func loadConfig() {
|
||||
config, err := beego.AppConfig.GetSection("mail")
|
||||
// Ping tests the connection and authentication with email server
|
||||
// If tls is true, a secure connection is established, or Ping
|
||||
// trys to upgrate the insecure connection to a secure one if
|
||||
// email server supports it.
|
||||
// Ping doesn't verify the server's certificate and hostname when
|
||||
// needed if the parameter insecure is ture
|
||||
func Ping(addr, identity, username, password string,
|
||||
timeout int, tls, insecure bool) (err error) {
|
||||
log.Debugf("establishing TCP connection with %s ...", addr)
|
||||
conn, err := net.DialTimeout("tcp", addr,
|
||||
time.Duration(timeout)*time.Second)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var useTLS = false
|
||||
if config["ssl"] != "" && strings.ToLower(config["ssl"]) == "true" {
|
||||
useTLS = true
|
||||
if tls {
|
||||
log.Debugf("establishing SSL/TLS connection with %s ...", addr)
|
||||
tlsConn := tlspkg.Client(conn, &tlspkg.Config{
|
||||
ServerName: host,
|
||||
InsecureSkipVerify: insecure,
|
||||
})
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return
|
||||
}
|
||||
defer tlsConn.Close()
|
||||
|
||||
conn = tlsConn
|
||||
}
|
||||
mc = MailConfig{
|
||||
Identity: config["identity"],
|
||||
Host: config["host"],
|
||||
Port: config["port"],
|
||||
Username: config["username"],
|
||||
Password: config["password"],
|
||||
TLS: useTLS,
|
||||
|
||||
log.Debugf("creating SMTP client for %s ...", host)
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
//try to swith to SSL/TLS
|
||||
if !tls {
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
log.Debugf("switching the connection with %s to SSL/TLS ...", addr)
|
||||
if err = client.StartTLS(&tlspkg.Config{
|
||||
ServerName: host,
|
||||
InsecureSkipVerify: insecure,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Debugf("the email server %s does not support STARTTLS", addr)
|
||||
}
|
||||
}
|
||||
|
||||
if ok, _ := client.Extension("AUTH"); ok {
|
||||
log.Debug("authenticating the client...")
|
||||
// only support plain auth
|
||||
if err = client.Auth(smtp.PlainAuth(identity,
|
||||
username, password, host)); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Debugf("the email server %s does not support AUTH, skip",
|
||||
addr)
|
||||
}
|
||||
|
||||
log.Debug("ping email server successfully")
|
||||
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
42
src/common/utils/email/mail_test.go
Normal file
42
src/common/utils/email/mail_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 email
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
addr := "smtp.gmail.com:465"
|
||||
identity := ""
|
||||
username := "wrong_username"
|
||||
password := "wrong_password"
|
||||
timeout := 60
|
||||
tls := true
|
||||
insecure := false
|
||||
|
||||
// test secure connection
|
||||
err := Ping(addr, identity, username, password,
|
||||
timeout, tls, insecure)
|
||||
if err == nil {
|
||||
t.Errorf("there should be an auth error")
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), "535") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
82
src/common/utils/notary/helper.go
Normal file
82
src/common/utils/notary/helper.go
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
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 notary
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/notary"
|
||||
"github.com/docker/notary/client"
|
||||
"github.com/docker/notary/trustpinning"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
notaryEndpoint = "http://notary-server:4443"
|
||||
notaryCachePath = "/root/notary"
|
||||
trustPin trustpinning.TrustPinConfig
|
||||
mockRetriever notary.PassRetriever
|
||||
)
|
||||
|
||||
// Target represents the json object of a target of a docker image in notary.
|
||||
// The struct will be used when repository is know so it won'g contain the name of a repository.
|
||||
type Target struct {
|
||||
Tag string `json:"tag"`
|
||||
Hashes data.Hashes `json:"hashes"`
|
||||
//TODO: update fields as needed.
|
||||
}
|
||||
|
||||
func init() {
|
||||
mockRetriever = func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) {
|
||||
passphrase = "hardcode"
|
||||
giveup = false
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
trustPin = trustpinning.TrustPinConfig{}
|
||||
}
|
||||
|
||||
// GetTargets is a help function called by API to fetch signature information of a given repository.
|
||||
// Per docker's convention the repository should contain the information of endpoint, i.e. it should look
|
||||
// like "10.117.4.117/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo)
|
||||
func GetTargets(username string, fqRepo string) ([]Target, error) {
|
||||
res := []Target{}
|
||||
authorizer := auth.NewNotaryUsernameTokenAuthorizer(username, "repository", fqRepo, "pull")
|
||||
store, err := auth.NewAuthorizerStore(strings.Split(notaryEndpoint, "//")[1], true, authorizer)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
tr := registry.NewTransport(registry.GetHTTPTransport(true), store)
|
||||
gun := data.GUN(fqRepo)
|
||||
notaryRepo, err := client.NewFileCachedNotaryRepository(notaryCachePath, gun, notaryEndpoint, tr, mockRetriever, trustPin)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
targets, err := notaryRepo.ListTargets(data.CanonicalTargetsRole)
|
||||
if _, ok := err.(client.ErrRepositoryNotExist); ok {
|
||||
log.Errorf("Repository not exist, repo: %s, error: %v, returning empty signature", fqRepo, err)
|
||||
return res, nil
|
||||
} else if err != nil {
|
||||
return res, err
|
||||
}
|
||||
for _, t := range targets {
|
||||
res = append(res, Target{t.Name, t.Hashes})
|
||||
}
|
||||
return res, nil
|
||||
}
|
31
src/common/utils/notary/helper_test.go
Normal file
31
src/common/utils/notary/helper_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package notary
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
notarytest "github.com/vmware/harbor/src/common/utils/notary/test"
|
||||
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var endpoint = "10.117.4.142"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
notaryServer := notarytest.NewNotaryServer(endpoint)
|
||||
defer notaryServer.Close()
|
||||
notaryEndpoint = notaryServer.URL
|
||||
notaryCachePath = "/tmp/notary"
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestGetTargets(t *testing.T) {
|
||||
targets, err := GetTargets("admin", path.Join(endpoint, "notary-demo/busybox"))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error: %v", err))
|
||||
assert.Equal(t, 1, len(targets), "")
|
||||
assert.Equal(t, "1.0", targets[0].Tag, "")
|
||||
|
||||
targets, err = GetTargets("admin", path.Join(endpoint, "notary-demo/notexist"))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error: %v", err))
|
||||
assert.Equal(t, 0, len(targets), "Targets list should be empty for non exist repo.")
|
||||
}
|
44
src/common/utils/notary/test/server.go
Normal file
44
src/common/utils/notary/test/server.go
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func currPath() string {
|
||||
_, f, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("Failed to get current directory")
|
||||
}
|
||||
return path.Dir(f)
|
||||
}
|
||||
|
||||
// NewNotaryServer creates a notary server for testing.
|
||||
func NewNotaryServer(endpoint string) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
validRoot := fmt.Sprintf("/v2/%s/notary-demo/busybox/_trust/tuf/", endpoint)
|
||||
invalidRoot := fmt.Sprintf("/v2/%s/notary-demo/fail/_trust/tuf/", endpoint)
|
||||
p := currPath()
|
||||
fmt.Printf("valid web root: %s, local path: %s\n", validRoot, path.Join(p, "valid"))
|
||||
mux.Handle(validRoot, http.StripPrefix(validRoot, http.FileServer(http.Dir(path.Join(p, "valid")))))
|
||||
mux.Handle(invalidRoot, http.StripPrefix(invalidRoot, http.FileServer(http.Dir(path.Join(p, "invalid")))))
|
||||
return httptest.NewServer(mux)
|
||||
}
|
1
src/common/utils/notary/test/valid/root.json
Normal file
1
src/common/utils/notary/test/valid/root.json
Normal file
@ -0,0 +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=="}]}
|
@ -0,0 +1 @@
|
||||
{"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=="}]}
|
@ -0,0 +1 @@
|
||||
{"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
src/common/utils/notary/test/valid/timestamp.json
Normal file
1
src/common/utils/notary/test/valid/timestamp.json
Normal file
@ -0,0 +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=="}]}
|
@ -252,9 +252,21 @@ type usernameTokenAuthorizer struct {
|
||||
username string
|
||||
}
|
||||
|
||||
// NewUsernameTokenAuthorizer returns a authorizer which will generate a token according to
|
||||
// NewRegistryUsernameTokenAuthorizer returns an authorizer to generate token for registry according to
|
||||
// the user's privileges
|
||||
func NewUsernameTokenAuthorizer(username string, scopeType, scopeName string, scopeActions ...string) Authorizer {
|
||||
func NewRegistryUsernameTokenAuthorizer(username, scopeType, scopeName string, scopeActions ...string) Authorizer {
|
||||
return newUsernameTokenAuthorizer(false, username, scopeType, scopeName, scopeActions...)
|
||||
}
|
||||
|
||||
// NewNotaryUsernameTokenAuthorizer returns an authorizer to generate token for notary according to
|
||||
// the user's privileges
|
||||
func NewNotaryUsernameTokenAuthorizer(username, scopeType, scopeName string, scopeActions ...string) Authorizer {
|
||||
return newUsernameTokenAuthorizer(true, username, scopeType, scopeName, scopeActions...)
|
||||
}
|
||||
|
||||
// newUsernameTokenAuthorizer returns a authorizer which will generate a token according to
|
||||
// the user's privileges
|
||||
func newUsernameTokenAuthorizer(notary bool, username, scopeType, scopeName string, scopeActions ...string) Authorizer {
|
||||
authorizer := &usernameTokenAuthorizer{
|
||||
username: username,
|
||||
}
|
||||
@ -264,13 +276,25 @@ func NewUsernameTokenAuthorizer(username string, scopeType, scopeName string, sc
|
||||
Name: scopeName,
|
||||
Actions: scopeActions,
|
||||
}
|
||||
|
||||
authorizer.tg = authorizer.generateToken
|
||||
|
||||
if notary {
|
||||
authorizer.tg = authorizer.genNotaryToken
|
||||
} else {
|
||||
authorizer.tg = authorizer.genRegistryToken
|
||||
}
|
||||
return authorizer
|
||||
}
|
||||
|
||||
func (u *usernameTokenAuthorizer) generateToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
|
||||
token, expiresIn, issuedAt, err = token_util.GenTokenForUI(u.username, service, scopes)
|
||||
token, expiresIn, issuedAt, err = token_util.RegistryTokenForUI(u.username, service, scopes)
|
||||
return
|
||||
}
|
||||
|
||||
func (u *usernameTokenAuthorizer) genRegistryToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
|
||||
token, expiresIn, issuedAt, err = token_util.RegistryTokenForUI(u.username, service, scopes)
|
||||
return
|
||||
}
|
||||
|
||||
func (u *usernameTokenAuthorizer) genNotaryToken(realm, service string, scopes []string) (token string, expiresIn int, issuedAt *time.Time, err error) {
|
||||
token, expiresIn, issuedAt, err = token_util.NotaryTokenForUI(u.username, service, scopes)
|
||||
return
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ var adminServerDefaultConfig = map[string]interface{}{
|
||||
config.MaxJobWorkers: 3,
|
||||
config.TokenExpiration: 30,
|
||||
config.CfgExpiration: 5,
|
||||
config.JobLogDir: "/var/log/jobs",
|
||||
config.UseCompressedJS: true,
|
||||
config.AdminInitialPassword: "password",
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
|
||||
const (
|
||||
defaultKeyPath string = "/etc/jobservice/key"
|
||||
defaultLogDir string = "/var/log/jobs"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -119,12 +120,12 @@ func LocalRegURL() (string, error) {
|
||||
}
|
||||
|
||||
// LogDir returns the absolute path to which the log file will be written
|
||||
func LogDir() (string, error) {
|
||||
cfg, err := mg.Get()
|
||||
if err != nil {
|
||||
return "", err
|
||||
func LogDir() string {
|
||||
dir := os.Getenv("LOG_DIR")
|
||||
if len(dir) == 0 {
|
||||
dir = defaultLogDir
|
||||
}
|
||||
return cfg[comcfg.JobLogDir].(string), nil
|
||||
return dir
|
||||
}
|
||||
|
||||
// SecretKey will return the secret key for encryption/decryption password in target.
|
||||
|
@ -66,8 +66,8 @@ func TestConfig(t *testing.T) {
|
||||
t.Fatalf("failed to get registry URL: %v", err)
|
||||
}
|
||||
|
||||
if _, err := LogDir(); err != nil {
|
||||
t.Fatalf("failed to get log directory: %v", err)
|
||||
if dir := LogDir(); dir != "/var/log/jobs" {
|
||||
t.Errorf("unexpected log directory: %s != %s", dir, "/var/log/jobs")
|
||||
}
|
||||
|
||||
if _, err := SecretKey(); err != nil {
|
||||
|
@ -65,10 +65,6 @@ func GetJobLogPath(jobID int64) (string, error) {
|
||||
|
||||
p = filepath.Join(d, p)
|
||||
}
|
||||
base, err := config.LogDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p = filepath.Join(base, p, f)
|
||||
p = filepath.Join(config.LogDir(), p, f)
|
||||
return p, nil
|
||||
}
|
||||
|
117
src/ui/api/email.go
Normal file
117
src/ui/api/email.go
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
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 (
|
||||
"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"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
const (
|
||||
pingEmailTimeout = 60
|
||||
)
|
||||
|
||||
// EmailAPI ...
|
||||
type EmailAPI struct {
|
||||
api.BaseAPI
|
||||
}
|
||||
|
||||
// Prepare ...
|
||||
func (e *EmailAPI) Prepare() {
|
||||
userID := e.ValidateUser()
|
||||
isSysAdmin, err := dao.IsAdminRole(userID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to check the role of user: %v", err)
|
||||
e.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if !isSysAdmin {
|
||||
e.CustomAbort(http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
settings.Host = host
|
||||
}
|
||||
|
||||
port, ok := m[comcfg.EmailPort]
|
||||
if ok {
|
||||
if len(port) == 0 {
|
||||
e.CustomAbort(http.StatusBadRequest, "empty email server port")
|
||||
}
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil || p <= 0 {
|
||||
e.CustomAbort(http.StatusBadRequest, "invalid email server port")
|
||||
}
|
||||
settings.Port = p
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Debugf("ping %s failed: %v", addr, err)
|
||||
e.CustomAbort(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
63
src/ui/api/email_test.go
Normal file
63
src/ui/api/email_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
comcfg "github.com/vmware/harbor/src/common/config"
|
||||
)
|
||||
|
||||
func TestPingEmail(t *testing.T) {
|
||||
fmt.Println("Testing ping email server")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
//case 1: ping email server without admin role
|
||||
code, _, err := apiTest.PingEmail(*testUser, nil)
|
||||
if err != nil {
|
||||
t.Errorf("failed to test ping email server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
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: secure connection with admin role
|
||||
code, body, err := apiTest.PingEmail(*admin, settings)
|
||||
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")
|
||||
|
||||
if !strings.Contains(body, "535") {
|
||||
t.Errorf("unexpected error: %s does not contains 535", body)
|
||||
return
|
||||
}
|
||||
}
|
@ -100,6 +100,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/email/ping", &EmailAPI{}, "post:Ping")
|
||||
|
||||
_ = updateInitPassword(1, "Harbor12345")
|
||||
|
||||
@ -452,7 +453,7 @@ func (a testapi) PutProjectMember(authInfo usrInfo, projectID string, userID str
|
||||
|
||||
//-------------------------Repositories Test---------------------------------------//
|
||||
//Return relevant repos of projectID
|
||||
func (a testapi) GetRepos(authInfo usrInfo, projectID string) (int, error) {
|
||||
func (a testapi) GetRepos(authInfo usrInfo, projectID, detail string) (int, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
|
||||
path := "/api/repositories/"
|
||||
@ -461,9 +462,13 @@ func (a testapi) GetRepos(authInfo usrInfo, projectID string) (int, error) {
|
||||
|
||||
type QueryParams struct {
|
||||
ProjectID string `url:"project_id"`
|
||||
Detail string `url:"detail"`
|
||||
}
|
||||
|
||||
_sling = _sling.QueryStruct(&QueryParams{ProjectID: projectID})
|
||||
_sling = _sling.QueryStruct(&QueryParams{
|
||||
ProjectID: projectID,
|
||||
Detail: detail,
|
||||
})
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
return httpStatusCode, err
|
||||
}
|
||||
@ -943,3 +948,11 @@ 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)
|
||||
|
||||
code, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
|
||||
return code, string(body), err
|
||||
}
|
||||
|
@ -19,23 +19,24 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/vmware/harbor/src/common/api"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/ui/service/cache"
|
||||
svc_utils "github.com/vmware/harbor/src/ui/service/utils"
|
||||
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/notary"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
svc_utils "github.com/vmware/harbor/src/ui/service/utils"
|
||||
)
|
||||
|
||||
// RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
|
||||
@ -44,6 +45,19 @@ type RepositoryAPI struct {
|
||||
api.BaseAPI
|
||||
}
|
||||
|
||||
type repoResp struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID int64 `json:"owner_id"`
|
||||
ProjectID int64 `json:"project_id"`
|
||||
Description string `json:"description"`
|
||||
PullCount int64 `json:"pull_count"`
|
||||
StarCount int64 `json:"star_count"`
|
||||
TagsCount int64 `json:"tags_count"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (ra *RepositoryAPI) Get() {
|
||||
projectID, err := ra.GetInt64("project_id")
|
||||
@ -51,8 +65,6 @@ func (ra *RepositoryAPI) Get() {
|
||||
ra.CustomAbort(http.StatusBadRequest, "invalid project_id")
|
||||
}
|
||||
|
||||
page, pageSize := ra.GetPaginationParams()
|
||||
|
||||
project, err := dao.GetProjectByID(projectID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project %d: %v", projectID, err)
|
||||
@ -77,30 +89,71 @@ func (ra *RepositoryAPI) Get() {
|
||||
}
|
||||
}
|
||||
|
||||
repositories, err := getReposByProject(project.Name, ra.GetString("q"))
|
||||
keyword := ra.GetString("q")
|
||||
|
||||
total, err := dao.GetTotalOfRepositoriesByProject(projectID, keyword)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get total of repositories of project %d: %v", projectID, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
|
||||
page, pageSize := ra.GetPaginationParams()
|
||||
|
||||
detail := ra.GetString("detail") == "1" || ra.GetString("detail") == "true"
|
||||
|
||||
repositories, err := getRepositories(projectID,
|
||||
keyword, pageSize, pageSize*(page-1), detail)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get repository: %v", err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
|
||||
total := int64(len(repositories))
|
||||
|
||||
if (page-1)*pageSize > total {
|
||||
repositories = []string{}
|
||||
} else {
|
||||
repositories = repositories[(page-1)*pageSize:]
|
||||
}
|
||||
|
||||
if page*pageSize <= total {
|
||||
repositories = repositories[:pageSize]
|
||||
}
|
||||
|
||||
ra.SetPaginationHeader(total, page, pageSize)
|
||||
|
||||
ra.Data["json"] = repositories
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
func getRepositories(projectID int64, keyword string,
|
||||
limit, offset int64, detail bool) (interface{}, error) {
|
||||
repositories, err := dao.GetRepositoriesByProject(projectID, keyword, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//keep compatibility with old API
|
||||
if !detail {
|
||||
result := []string{}
|
||||
for _, repository := range repositories {
|
||||
result = append(result, repository.Name)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result := []*repoResp{}
|
||||
for _, repository := range repositories {
|
||||
repo := &repoResp{
|
||||
ID: repository.RepositoryID,
|
||||
Name: repository.Name,
|
||||
OwnerID: repository.OwnerID,
|
||||
ProjectID: repository.ProjectID,
|
||||
Description: repository.Description,
|
||||
PullCount: repository.PullCount,
|
||||
StarCount: repository.StarCount,
|
||||
CreationTime: repository.CreationTime,
|
||||
UpdateTime: repository.UpdateTime,
|
||||
}
|
||||
|
||||
tags, err := getTags(repository.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo.TagsCount = int64(len(tags))
|
||||
result = append(result, repo)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (ra *RepositoryAPI) Delete() {
|
||||
repoName := ra.GetString("repo_name")
|
||||
@ -119,11 +172,9 @@ func (ra *RepositoryAPI) Delete() {
|
||||
ra.CustomAbort(http.StatusNotFound, fmt.Sprintf("project %s not found", projectName))
|
||||
}
|
||||
|
||||
if project.Public == 0 {
|
||||
userID := ra.ValidateUser()
|
||||
if !hasProjectAdminRole(userID, project.ProjectID) {
|
||||
ra.CustomAbort(http.StatusForbidden, "")
|
||||
}
|
||||
userID := ra.ValidateUser()
|
||||
if !hasProjectAdminRole(userID, project.ProjectID) {
|
||||
ra.CustomAbort(http.StatusForbidden, "")
|
||||
}
|
||||
|
||||
rc, err := ra.initRepositoryClient(repoName)
|
||||
@ -195,13 +246,6 @@ func (ra *RepositoryAPI) Delete() {
|
||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Debug("refreshing catalog cache")
|
||||
if err := cache.RefreshCatalogCache(); err != nil {
|
||||
log.Errorf("error occurred while refresh catalog cache: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type tag struct {
|
||||
@ -234,36 +278,46 @@ func (ra *RepositoryAPI) GetTags() {
|
||||
}
|
||||
}
|
||||
|
||||
rc, err := ra.initRepositoryClient(repoName)
|
||||
client, err := ra.initRepositoryClient(repoName)
|
||||
if err != nil {
|
||||
log.Errorf("error occurred while initializing repository client for %s: %v", repoName, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
|
||||
tags := []string{}
|
||||
|
||||
ts, err := rc.ListTag()
|
||||
tags, err := listTag(client)
|
||||
if err != nil {
|
||||
regErr, ok := err.(*registry_error.Error)
|
||||
if !ok {
|
||||
log.Errorf("error occurred while listing tags of %s: %v", repoName, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
|
||||
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
ra.Data["json"] = tags
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
func listTag(client *registry.Repository) ([]string, error) {
|
||||
tags := []string{}
|
||||
|
||||
ts, err := client.ListTag()
|
||||
if err != nil {
|
||||
// TODO remove the logic if the bug of registry is fixed
|
||||
// It's a workaround for a bug of registry: when listing tags of
|
||||
// a repository which is being pushed, a "NAME_UNKNOWN" error will
|
||||
// been returned, while the catalog API can list this repository.
|
||||
if regErr.StatusCode != http.StatusNotFound {
|
||||
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
|
||||
if regErr, ok := err.(*registry_error.Error); ok &&
|
||||
regErr.StatusCode == http.StatusNotFound {
|
||||
return tags, nil
|
||||
}
|
||||
}
|
||||
|
||||
tags = append(tags, ts...)
|
||||
|
||||
sort.Strings(tags)
|
||||
|
||||
ra.Data["json"] = tags
|
||||
ra.ServeJSON()
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetManifests handles GET /api/repositories/manifests
|
||||
@ -382,7 +436,7 @@ func (ra *RepositoryAPI) initRepositoryClient(repoName string) (r *registry.Repo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cache.NewRepositoryClient(endpoint, !verify, username, repoName,
|
||||
return NewRepositoryClient(endpoint, !verify, username, repoName,
|
||||
"repository", repoName, "pull", "push", "*")
|
||||
}
|
||||
|
||||
@ -438,6 +492,34 @@ func (ra *RepositoryAPI) GetTopRepos() {
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
//GetSignatures handles request GET /api/repositories/signatures
|
||||
func (ra *RepositoryAPI) GetSignatures() {
|
||||
//use this func to init session.
|
||||
ra.GetUserIDForRequest()
|
||||
repoName := ra.GetString("repo_name")
|
||||
if len(repoName) == 0 {
|
||||
ra.CustomAbort(http.StatusBadRequest, "repo_name is nil")
|
||||
}
|
||||
ext, err := config.ExtEndpoint()
|
||||
if err != nil {
|
||||
log.Errorf("Error while reading external endpoint: %v", err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
endpoint := strings.Split(ext, "//")[1]
|
||||
fqRepo := path.Join(endpoint, repoName)
|
||||
username, err := ra.getUsername()
|
||||
if err != nil {
|
||||
log.Warningf("Error when getting username: %v", err)
|
||||
}
|
||||
targets, err := notary.GetTargets(username, fqRepo)
|
||||
if err != nil {
|
||||
log.Errorf("Error while fetching signature from notary: %v", err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal error")
|
||||
}
|
||||
ra.Data["json"] = targets
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
func newRepositoryClient(endpoint string, insecure bool, username, password, repository, scopeType, scopeName string,
|
||||
scopeActions ...string) (*registry.Repository, error) {
|
||||
|
||||
|
@ -20,7 +20,7 @@ func TestGetRepos(t *testing.T) {
|
||||
fmt.Println("Testing Repos Get API")
|
||||
//-------------------case 1 : response code = 200------------------------//
|
||||
fmt.Println("case 1 : response code = 200")
|
||||
httpStatusCode, err = apiTest.GetRepos(*admin, projectID)
|
||||
httpStatusCode, err = apiTest.GetRepos(*admin, projectID, "true")
|
||||
if err != nil {
|
||||
t.Error("Error whihle get repos by projectID", err.Error())
|
||||
t.Log(err)
|
||||
@ -30,7 +30,7 @@ func TestGetRepos(t *testing.T) {
|
||||
//-------------------case 2 : response code = 400------------------------//
|
||||
fmt.Println("case 2 : response code = 409,invalid project_id")
|
||||
projectID = "ccc"
|
||||
httpStatusCode, err = apiTest.GetRepos(*admin, projectID)
|
||||
httpStatusCode, err = apiTest.GetRepos(*admin, projectID, "0")
|
||||
if err != nil {
|
||||
t.Error("Error whihle get repos by projectID", err.Error())
|
||||
t.Log(err)
|
||||
@ -40,7 +40,7 @@ func TestGetRepos(t *testing.T) {
|
||||
//-------------------case 3 : response code = 404------------------------//
|
||||
fmt.Println("case 3 : response code = 404:project not found")
|
||||
projectID = "111"
|
||||
httpStatusCode, err = apiTest.GetRepos(*admin, projectID)
|
||||
httpStatusCode, err = apiTest.GetRepos(*admin, projectID, "0")
|
||||
if err != nil {
|
||||
t.Error("Error whihle get repos by projectID", err.Error())
|
||||
t.Log(err)
|
||||
|
@ -20,12 +20,12 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/common/api"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/ui/service/cache"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/api"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
// SearchAPI handles requesst to /api/search
|
||||
@ -34,7 +34,7 @@ type SearchAPI struct {
|
||||
}
|
||||
|
||||
type searchResult struct {
|
||||
Project []map[string]interface{} `json:"project"`
|
||||
Project []models.Project `json:"project"`
|
||||
Repository []map[string]interface{} `json:"repository"`
|
||||
}
|
||||
|
||||
@ -71,58 +71,104 @@ func (s *SearchAPI) Get() {
|
||||
|
||||
projectSorter := &models.ProjectSorter{Projects: projects}
|
||||
sort.Sort(projectSorter)
|
||||
projectResult := []map[string]interface{}{}
|
||||
projectResult := []models.Project{}
|
||||
for _, p := range projects {
|
||||
match := true
|
||||
if len(keyword) > 0 && !strings.Contains(p.Name, keyword) {
|
||||
match = false
|
||||
}
|
||||
if match {
|
||||
entry := make(map[string]interface{})
|
||||
entry["id"] = p.ProjectID
|
||||
entry["name"] = p.Name
|
||||
entry["public"] = p.Public
|
||||
projectResult = append(projectResult, entry)
|
||||
if userID != dao.NonExistUserID {
|
||||
if isSysAdmin {
|
||||
p.Role = models.PROJECTADMIN
|
||||
}
|
||||
if p.Role == models.PROJECTADMIN {
|
||||
p.Togglable = true
|
||||
}
|
||||
}
|
||||
|
||||
repos, err := dao.GetRepositoryByProjectName(p.Name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get repositories of project %s: %v", p.Name, err)
|
||||
s.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
|
||||
p.RepoCount = len(repos)
|
||||
|
||||
projectResult = append(projectResult, p)
|
||||
}
|
||||
}
|
||||
|
||||
repositories, err := cache.GetRepoFromCache()
|
||||
repositoryResult, err := filterRepositories(projects, keyword)
|
||||
if err != nil {
|
||||
log.Errorf("failed to list repositories: %v", err)
|
||||
log.Errorf("failed to filter repositories: %v", err)
|
||||
s.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
|
||||
sort.Strings(repositories)
|
||||
repositoryResult := filterRepositories(repositories, projects, keyword)
|
||||
result := &searchResult{Project: projectResult, Repository: repositoryResult}
|
||||
s.Data["json"] = result
|
||||
s.ServeJSON()
|
||||
}
|
||||
|
||||
func filterRepositories(repositories []string, projects []models.Project, keyword string) []map[string]interface{} {
|
||||
func filterRepositories(projects []models.Project, keyword string) (
|
||||
[]map[string]interface{}, error) {
|
||||
|
||||
repositories, err := dao.GetAllRepositories()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i, j := 0, 0
|
||||
result := []map[string]interface{}{}
|
||||
for i < len(repositories) && j < len(projects) {
|
||||
r := repositories[i]
|
||||
p, _ := utils.ParseRepository(r)
|
||||
p, _ := utils.ParseRepository(r.Name)
|
||||
d := strings.Compare(p, projects[j].Name)
|
||||
if d < 0 {
|
||||
i++
|
||||
continue
|
||||
} else if d == 0 {
|
||||
i++
|
||||
if len(keyword) != 0 && !strings.Contains(r, keyword) {
|
||||
if len(keyword) != 0 && !strings.Contains(r.Name, keyword) {
|
||||
continue
|
||||
}
|
||||
entry := make(map[string]interface{})
|
||||
entry["repository_name"] = r
|
||||
entry["repository_name"] = r.Name
|
||||
entry["project_name"] = projects[j].Name
|
||||
entry["project_id"] = projects[j].ProjectID
|
||||
entry["project_public"] = projects[j].Public
|
||||
entry["pull_count"] = r.PullCount
|
||||
|
||||
tags, err := getTags(r.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry["tags_count"] = len(tags)
|
||||
|
||||
result = append(result, entry)
|
||||
} else {
|
||||
j++
|
||||
}
|
||||
}
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getTags(repository string) ([]string, error) {
|
||||
url, err := config.RegistryURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := NewRepositoryClient(url, true,
|
||||
"admin", repository, "repository", repository, "pull")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := listTag(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
@ -22,9 +22,9 @@ func TestSearch(t *testing.T) {
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
assert.Equal(int64(1), result.Projects[0].Id, "Project id should be equal")
|
||||
assert.Equal(int64(1), result.Projects[0].ProjectID, "Project id should be equal")
|
||||
assert.Equal("library", result.Projects[0].Name, "Project name should be library")
|
||||
assert.Equal(int32(1), result.Projects[0].Public, "Project public status should be 1 (true)")
|
||||
assert.Equal(1, result.Projects[0].Public, "Project public status should be 1 (true)")
|
||||
}
|
||||
|
||||
//--------case 2 : Response Code = 200, sysAdmin and search repo--------//
|
||||
|
@ -29,9 +29,9 @@ import (
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/registry/error"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
"github.com/vmware/harbor/src/ui/service/cache"
|
||||
)
|
||||
|
||||
func checkProjectPermission(userID int, projectID int64) bool {
|
||||
@ -352,7 +352,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
|
||||
if err != nil {
|
||||
return needsAdd, needsDel, err
|
||||
}
|
||||
client, err := cache.NewRepositoryClient(endpoint, true,
|
||||
client, err := NewRepositoryClient(endpoint, true,
|
||||
"admin", repoInR, "repository", repoInR)
|
||||
if err != nil {
|
||||
return needsAdd, needsDel, err
|
||||
@ -377,7 +377,7 @@ func diffRepos(reposInRegistry []string, reposInDB []string) ([]string, []string
|
||||
if err != nil {
|
||||
return needsAdd, needsDel, err
|
||||
}
|
||||
client, err := cache.NewRepositoryClient(endpoint, true,
|
||||
client, err := NewRepositoryClient(endpoint, true,
|
||||
"admin", repoInR, "repository", repoInR)
|
||||
if err != nil {
|
||||
return needsAdd, needsDel, err
|
||||
@ -440,7 +440,7 @@ func initRegistryClient() (r *registry.Registry, err error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
registryClient, err := cache.NewRegistryClient(endpoint, true, "admin",
|
||||
registryClient, err := NewRegistryClient(endpoint, true, "admin",
|
||||
"registry", "catalog", "*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -485,10 +485,6 @@ func getReposByProject(name string, keyword ...string) ([]string, error) {
|
||||
return repositories, nil
|
||||
}
|
||||
|
||||
func getAllRepos() ([]string, error) {
|
||||
return cache.GetRepoFromCache()
|
||||
}
|
||||
|
||||
func repositoryExist(name string, client *registry.Repository) (bool, error) {
|
||||
tags, err := client.ListTag()
|
||||
if err != nil {
|
||||
@ -499,3 +495,38 @@ func repositoryExist(name string, client *registry.Repository) (bool, error) {
|
||||
}
|
||||
return len(tags) != 0, nil
|
||||
}
|
||||
|
||||
// NewRegistryClient ...
|
||||
func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string,
|
||||
scopeActions ...string) (*registry.Registry, error) {
|
||||
authorizer := auth.NewRegistryUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
|
||||
|
||||
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewRepositoryClient ...
|
||||
func NewRepositoryClient(endpoint string, insecure bool, username, repository, scopeType, scopeName string,
|
||||
scopeActions ...string) (*registry.Repository, error) {
|
||||
|
||||
authorizer := auth.NewRegistryUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
|
||||
|
||||
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ func initRouters() {
|
||||
beego.Router("/api/repositories", &api.RepositoryAPI{})
|
||||
beego.Router("/api/repositories/tags", &api.RepositoryAPI{}, "get:GetTags")
|
||||
beego.Router("/api/repositories/manifests", &api.RepositoryAPI{}, "get:GetManifests")
|
||||
beego.Router("/api/repositories/signatures", &api.RepositoryAPI{}, "get:GetSignatures")
|
||||
beego.Router("/api/jobs/replication/", &api.RepJobAPI{}, "get:List")
|
||||
beego.Router("/api/jobs/replication/:id([0-9]+)", &api.RepJobAPI{})
|
||||
beego.Router("/api/jobs/replication/:id([0-9]+)/log", &api.RepJobAPI{}, "get:GetLog")
|
||||
@ -96,6 +97,7 @@ func initRouters() {
|
||||
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
|
||||
beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "post:Search")
|
||||
beego.Router("/api/ldap/users/import", &api.LdapAPI{}, "post:ImportUser")
|
||||
beego.Router("/api/email/ping", &api.EmailAPI{}, "post:Ping")
|
||||
|
||||
//external service that hosted on harbor process:
|
||||
beego.Router("/service/notifications", &service.NotificationHandler{})
|
||||
|
119
src/ui/service/cache/cache.go
vendored
119
src/ui/service/cache/cache.go
vendored
@ -1,119 +0,0 @@
|
||||
/*
|
||||
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 cache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
|
||||
"github.com/astaxie/beego/cache"
|
||||
)
|
||||
|
||||
var (
|
||||
// Cache is the global cache in system.
|
||||
Cache cache.Cache
|
||||
)
|
||||
|
||||
const catalogKey string = "catalog"
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
Cache, err = cache.NewCache("memory", `{"interval":720}`)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to initialize cache, error:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshCatalogCache calls registry's API to get repository list and write it to cache.
|
||||
func RefreshCatalogCache() error {
|
||||
log.Debug("refreshing catalog cache...")
|
||||
|
||||
repos, err := getAllRepositories()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Cache.Put(catalogKey, repos, 600*time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepoFromCache get repository list from cache, it refreshes the cache if it's empty.
|
||||
func GetRepoFromCache() ([]string, error) {
|
||||
|
||||
result := Cache.Get(catalogKey)
|
||||
if result == nil {
|
||||
err := RefreshCatalogCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cached := Cache.Get(catalogKey)
|
||||
if cached != nil {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
return result.([]string), nil
|
||||
}
|
||||
|
||||
func getAllRepositories() ([]string, error) {
|
||||
var repos []string
|
||||
rs, err := dao.GetAllRepositories()
|
||||
if err != nil {
|
||||
return repos, err
|
||||
}
|
||||
for _, e := range rs {
|
||||
repos = append(repos, e.Name)
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// NewRegistryClient ...
|
||||
func NewRegistryClient(endpoint string, insecure bool, username, scopeType, scopeName string,
|
||||
scopeActions ...string) (*registry.Registry, error) {
|
||||
authorizer := auth.NewUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
|
||||
|
||||
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := registry.NewRegistryWithModifiers(endpoint, insecure, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewRepositoryClient ...
|
||||
func NewRepositoryClient(endpoint string, insecure bool, username, repository, scopeType, scopeName string,
|
||||
scopeActions ...string) (*registry.Repository, error) {
|
||||
|
||||
authorizer := auth.NewUsernameTokenAuthorizer(username, scopeType, scopeName, scopeActions...)
|
||||
|
||||
store, err := auth.NewAuthorizerStore(endpoint, insecure, authorizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := registry.NewRepositoryWithModifiers(repository, endpoint, insecure, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
9
src/ui/service/cache/cache_test.go
vendored
9
src/ui/service/cache/cache_test.go
vendored
@ -1,9 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import (
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/api"
|
||||
"github.com/vmware/harbor/src/ui/service/cache"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
)
|
||||
@ -82,9 +81,6 @@ func (n *NotificationHandler) Post() {
|
||||
if err := dao.AddRepository(repoRecord); err != nil {
|
||||
log.Errorf("Error happens when adding repository: %v", err)
|
||||
}
|
||||
if err := cache.RefreshCatalogCache(); err != nil {
|
||||
log.Errorf("failed to refresh cache: %v", err)
|
||||
}
|
||||
}()
|
||||
go api.TriggerReplicationByRepository(repository, []string{tag}, models.RepOpTransfer)
|
||||
}
|
||||
|
@ -74,25 +74,49 @@ func GetResourceActions(scopes []string) []*token.ResourceActions {
|
||||
return res
|
||||
}
|
||||
|
||||
// GenTokenForUI is for the UI process to call, so it won't establish a https connection from UI to proxy.
|
||||
func GenTokenForUI(username string, service string, scopes []string) (string, int, *time.Time, error) {
|
||||
//filterAccess iterate a list of resource actions and try to use the filter that matches the resource type to filter the actions.
|
||||
func filterAccess(access []*token.ResourceActions, u userInfo, filters map[string]accessFilter) error {
|
||||
var err error
|
||||
for _, a := range access {
|
||||
f, ok := filters[a.Type]
|
||||
if !ok {
|
||||
a.Actions = []string{}
|
||||
log.Warningf("No filter found for access type: %s, skip filter, the access of resource '%s' will be set empty.", a.Type, a.Name)
|
||||
continue
|
||||
}
|
||||
err = f.filter(u, a)
|
||||
log.Debugf("user: %s, access: %v", u.name, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//RegistryTokenForUI calls genTokenForUI to get raw token for registry
|
||||
func RegistryTokenForUI(username string, service string, scopes []string) (string, int, *time.Time, error) {
|
||||
return genTokenForUI(username, service, scopes, registryFilterMap)
|
||||
}
|
||||
|
||||
//NotaryTokenForUI calls genTokenForUI to get raw token for notary
|
||||
func NotaryTokenForUI(username string, service string, scopes []string) (string, int, *time.Time, error) {
|
||||
return genTokenForUI(username, service, scopes, notaryFilterMap)
|
||||
}
|
||||
|
||||
// genTokenForUI is for the UI process to call, so it won't establish a https connection from UI to proxy.
|
||||
func genTokenForUI(username string, service string, scopes []string, filters map[string]accessFilter) (string, int, *time.Time, error) {
|
||||
isAdmin, err := dao.IsAdminRole(username)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
f := &repositoryFilter{
|
||||
parser: &basicParser{},
|
||||
}
|
||||
u := userInfo{
|
||||
name: username,
|
||||
allPerm: isAdmin,
|
||||
}
|
||||
access := GetResourceActions(scopes)
|
||||
for _, a := range access {
|
||||
err = f.filter(u, a)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
err = filterAccess(access, u, filters)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
return MakeRawToken(username, service, access)
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ import (
|
||||
)
|
||||
|
||||
var creatorMap map[string]Creator
|
||||
var registryFilterMap map[string]accessFilter
|
||||
var notaryFilterMap map[string]accessFilter
|
||||
|
||||
const (
|
||||
notary = "harbor-notary"
|
||||
@ -35,22 +37,29 @@ const (
|
||||
//InitCreators initialize the token creators for different services
|
||||
func InitCreators() {
|
||||
creatorMap = make(map[string]Creator)
|
||||
registryFilterMap = map[string]accessFilter{
|
||||
"repository": &repositoryFilter{
|
||||
parser: &basicParser{},
|
||||
},
|
||||
"registry": ®istryFilter{},
|
||||
}
|
||||
ext, err := config.ExtEndpoint()
|
||||
if err != nil {
|
||||
log.Warningf("Failed to get ext enpoint, err: %v, the token service will not be functional with notary requests", err)
|
||||
} else {
|
||||
notaryFilterMap = map[string]accessFilter{
|
||||
"repository": &repositoryFilter{
|
||||
parser: &endpointParser{
|
||||
endpoint: strings.Split(ext, "//")[1],
|
||||
},
|
||||
},
|
||||
}
|
||||
creatorMap[notary] = &generalCreator{
|
||||
validators: []ReqValidator{
|
||||
&basicAuthValidator{},
|
||||
},
|
||||
service: notary,
|
||||
filterMap: map[string]accessFilter{
|
||||
"repository": &repositoryFilter{
|
||||
parser: &endpointParser{
|
||||
endpoint: strings.Split(ext, "//")[1],
|
||||
},
|
||||
},
|
||||
},
|
||||
service: notary,
|
||||
filterMap: notaryFilterMap,
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,16 +68,8 @@ func InitCreators() {
|
||||
&secretValidator{config.JobserviceSecret()},
|
||||
&basicAuthValidator{},
|
||||
},
|
||||
service: registry,
|
||||
filterMap: map[string]accessFilter{
|
||||
"repository": &repositoryFilter{
|
||||
//Workaround, had to use same service for both notary and registry
|
||||
parser: &endpointParser{
|
||||
endpoint: ext,
|
||||
},
|
||||
},
|
||||
"registry": ®istryFilter{},
|
||||
},
|
||||
service: registry,
|
||||
filterMap: registryFilterMap,
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,15 +103,10 @@ func (e endpointParser) parse(s string) (*image, error) {
|
||||
if len(repo) < 2 {
|
||||
return nil, fmt.Errorf("Unable to parse image from string: %s", s)
|
||||
}
|
||||
//Workaround, need to use endpoint Parser to handle both cases.
|
||||
if strings.ContainsRune(repo[0], '.') {
|
||||
if repo[0] != e.endpoint {
|
||||
log.Warningf("Mismatch endpoint from string: %s, expected endpoint: %s, fallback to basic parser", s, e.endpoint)
|
||||
return parseImg(s)
|
||||
}
|
||||
return parseImg(repo[1])
|
||||
if repo[0] != e.endpoint {
|
||||
return nil, fmt.Errorf("Mismatch endpoint from string: %s, expected endpoint: %s", s, e.endpoint)
|
||||
}
|
||||
return parseImg(s)
|
||||
return parseImg(repo[1])
|
||||
}
|
||||
|
||||
//build Image accepts a string like library/ubuntu:14.04 and build a image struct
|
||||
@ -153,7 +149,6 @@ type repositoryFilter struct {
|
||||
|
||||
func (rep repositoryFilter) filter(user userInfo, a *token.ResourceActions) error {
|
||||
//clear action list to assign to new acess element after perm check.
|
||||
a.Actions = []string{}
|
||||
img, err := rep.parser.parse(a.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -224,16 +219,9 @@ func (g generalCreator) Create(r *http.Request) (*tokenJSON, error) {
|
||||
user = &userInfo{}
|
||||
}
|
||||
access := GetResourceActions(scopes)
|
||||
for _, a := range access {
|
||||
f, ok := g.filterMap[a.Type]
|
||||
if !ok {
|
||||
log.Warningf("No filter found for access type: %s, skip.", a.Type)
|
||||
continue
|
||||
}
|
||||
err = f.filter(*user, a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = filterAccess(access, *user, g.filterMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return makeToken(user.name, g.service, access)
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ func TestMain(m *testing.M) {
|
||||
if err := config.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
InitCreators()
|
||||
result := m.Run()
|
||||
if result != 0 {
|
||||
os.Exit(result)
|
||||
@ -169,9 +170,8 @@ func TestEndpointParser(t *testing.T) {
|
||||
}
|
||||
testList := []parserTestRec{parserTestRec{"10.117.4.142:5000/library/ubuntu:14.04", image{"library", "ubuntu", "14.04"}, false},
|
||||
parserTestRec{"myimage:14.04", image{}, true},
|
||||
//Test the temp workaround
|
||||
parserTestRec{"10.117.4.142:80/library/myimage:14.04", image{"10.117.4.142:80", "library/myimage", "14.04"}, false},
|
||||
parserTestRec{"library/myimage:14.04", image{"library", "myimage", "14.04"}, false},
|
||||
parserTestRec{"10.117.4.142:80/library/myimage:14.04", image{}, true},
|
||||
parserTestRec{"library/myimage:14.04", image{}, true},
|
||||
parserTestRec{"10.117.4.142:5000/myimage:14.04", image{}, true},
|
||||
parserTestRec{"10.117.4.142:5000/org/team/img", image{"org", "team/img", ""}, false},
|
||||
}
|
||||
@ -185,3 +185,28 @@ func TestEndpointParser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAccess(t *testing.T) {
|
||||
//TODO put initial data in DB to verify repository filter.
|
||||
var err error
|
||||
s := []string{"registry:catalog:*"}
|
||||
a1 := GetResourceActions(s)
|
||||
a2 := GetResourceActions(s)
|
||||
u := userInfo{"jack", false}
|
||||
ra1 := token.ResourceActions{
|
||||
Type: "registry",
|
||||
Name: "catalog",
|
||||
Actions: []string{"*"},
|
||||
}
|
||||
ra2 := token.ResourceActions{
|
||||
Type: "registry",
|
||||
Name: "catalog",
|
||||
Actions: []string{},
|
||||
}
|
||||
err = filterAccess(a1, u, registryFilterMap)
|
||||
assert.Nil(t, err, "Unexpected error: %v", err)
|
||||
assert.Equal(t, ra1, *a1[0], "Mismatch after registry filter Map")
|
||||
err = filterAccess(a2, u, notaryFilterMap)
|
||||
assert.Nil(t, err, "Unexpected error: %v", err)
|
||||
assert.Equal(t, ra2, *a2[0], "Mismatch after notary filter Map")
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
|
||||
[*.md]
|
||||
max_line_length = 0
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Indentation override
|
||||
#[lib/**.js]
|
||||
#[{package.json,.travis.yml}]
|
||||
#[**/**.js]
|
9
src/ui_ng/.gitignore
vendored
9
src/ui_ng/.gitignore
vendored
@ -1,9 +0,0 @@
|
||||
coverage/
|
||||
dist/
|
||||
html-report/
|
||||
node_modules/
|
||||
typings/
|
||||
**/*npm-debug.log.*
|
||||
**/*yarn-error.log.*
|
||||
.idea/
|
||||
.DS_Store
|
@ -1,10 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6.9"
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
@ -4,19 +4,19 @@
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
frameworks: ['jasmine', 'angular-cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-phantomjs-launcher'),
|
||||
require('karma-mocha-reporter'),
|
||||
require('karma-remap-istanbul'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
require('angular-cli/plugins/karma')
|
||||
],
|
||||
files: [
|
||||
{pattern: './src/test.ts', watched: false}
|
||||
],
|
||||
preprocessors: {
|
||||
'./src/test.ts': ['@angular/cli']
|
||||
'./src/test.ts': ['angular-cli']
|
||||
},
|
||||
mime: {
|
||||
'text/x-typescript': ['ts', 'tsx']
|
||||
|
@ -4,7 +4,7 @@
|
||||
"description": "Angular-CLI starter for a Clarity project",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"start": "ng serve",
|
||||
"start": "ng serve --host 0.0.0.0",
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
@ -12,7 +12,6 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cli": "^1.0.0-beta.30",
|
||||
"@angular/common": "^2.4.1",
|
||||
"@angular/compiler": "^2.4.1",
|
||||
"@angular/core": "^2.4.1",
|
||||
@ -21,11 +20,15 @@
|
||||
"@angular/platform-browser": "^2.4.1",
|
||||
"@angular/platform-browser-dynamic": "^2.4.1",
|
||||
"@angular/router": "^3.4.1",
|
||||
"@ngx-translate/core": "^6.0.0",
|
||||
"@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",
|
||||
"core-js": "^2.4.1",
|
||||
"fs": "0.0.1-security",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
@ -37,6 +40,7 @@
|
||||
"@types/core-js": "^0.9.34",
|
||||
"@types/jasmine": "^2.2.30",
|
||||
"@types/node": "^6.0.42",
|
||||
"angular-cli": "^1.0.0-beta.24",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"codelyzer": "~1.0.0-beta.3",
|
||||
"enhanced-resolve": "^3.0.0",
|
||||
|
@ -1,55 +1,48 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalSize]="'lg'">
|
||||
<h3 class="modal-title">User Profile</h3>
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop">
|
||||
<h3 class="modal-title">{{'PROFILE.TITLE' | translate}}</h3>
|
||||
<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">Username</label>
|
||||
<input type="text" name="account_settings_username" [(ngModel)]="account.username" disabled id="account_settings_username" size="51">
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_email" class="col-md-4 required">Email</label>
|
||||
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [class.invalid]="eamilInput.invalid && (eamilInput.dirty || eamilInput.touched)">
|
||||
<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)">
|
||||
<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="48">
|
||||
pattern='^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="account_settings_email" size="28">
|
||||
<span class="tooltip-content">
|
||||
Email should be a valid email address like name@example.com
|
||||
{{'TOOLTIP.EMAIL' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_full_name" class="col-md-4 required">Full name</label>
|
||||
<label for="account_settings_email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [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="48">
|
||||
<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">
|
||||
<span class="tooltip-content">
|
||||
Max length of full name is 20
|
||||
{{'TOOLTIP.FULL_NAME' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="account_settings_comments" class="col-md-4">Comments</label>
|
||||
<label for="account_settings_comments" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-right" [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="48">
|
||||
<label for="account_settings_comments" class="col-md-4">{{'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">
|
||||
<span class="tooltip-content">
|
||||
Length of comment should be less than 20
|
||||
{{'TOOLTIP.COMMENT' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<div style="height: 30px;"></div>
|
||||
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertClosable]="true" [(clrAlertClosed)]="alertClose">
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">
|
||||
{{errorMessage}}
|
||||
</span>
|
||||
</div>
|
||||
</clr-alert>
|
||||
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
|
||||
<button type="button" class="btn btn-outline" (click)="close()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="submit()">Ok</button>
|
||||
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="submit()">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -3,6 +3,10 @@ import { NgForm } from '@angular/forms';
|
||||
|
||||
import { SessionUser } from '../../shared/session-user';
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
import { MessageService } from '../../global-message/message.service';
|
||||
import { AlertType, httpStatusCode } from '../../shared/shared.const';
|
||||
import { errorHandler, accessErrorHandler } from '../../shared/shared.utils';
|
||||
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
|
||||
|
||||
@Component({
|
||||
selector: "account-settings-modal",
|
||||
@ -13,43 +17,52 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
opened: boolean = false;
|
||||
staticBackdrop: boolean = true;
|
||||
account: SessionUser;
|
||||
error: any;
|
||||
alertClose: boolean = true;
|
||||
error: any = null;
|
||||
originalStaticData: SessionUser;
|
||||
|
||||
private isOnCalling: boolean = false;
|
||||
private formValueChanged: boolean = false;
|
||||
|
||||
accountFormRef: NgForm;
|
||||
@ViewChild("accountSettingsFrom") accountForm: NgForm;
|
||||
@ViewChild(InlineAlertComponent)
|
||||
private inlineAlert: InlineAlertComponent;
|
||||
|
||||
constructor(private session: SessionService) { }
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private msgService: MessageService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
//Value copy
|
||||
this.account = Object.assign({}, this.session.getCurrentUser());
|
||||
}
|
||||
|
||||
private isUserDataChange(): boolean {
|
||||
if (!this.originalStaticData || !this.account) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var prop in this.originalStaticData) {
|
||||
if (this.originalStaticData[prop]) {
|
||||
if (this.account[prop]) {
|
||||
if (this.originalStaticData[prop] != this.account[prop]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.accountForm && this.accountForm.valid;
|
||||
return this.accountForm && this.accountForm.valid && this.error === null;
|
||||
}
|
||||
|
||||
public get showProgress(): boolean {
|
||||
return this.isOnCalling;
|
||||
}
|
||||
|
||||
public get errorMessage(): string {
|
||||
if(this.error){
|
||||
if(this.error.message){
|
||||
return this.error.message;
|
||||
}else{
|
||||
if(this.error._body){
|
||||
return this.error._body;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
if (this.accountFormRef != this.accountForm) {
|
||||
this.accountFormRef = this.accountForm;
|
||||
@ -59,12 +72,15 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
this.error = null;
|
||||
}
|
||||
this.formValueChanged = true;
|
||||
this.inlineAlert.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
//Keep the initial data for future diff
|
||||
this.originalStaticData = Object.assign({}, this.session.getCurrentUser());
|
||||
this.account = Object.assign({}, this.session.getCurrentUser());
|
||||
this.formValueChanged = false;
|
||||
|
||||
@ -72,7 +88,18 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.opened = false;
|
||||
if (this.formValueChanged) {
|
||||
if (!this.isUserDataChange()) {
|
||||
this.opened = false;
|
||||
} else {
|
||||
//Need user confirmation
|
||||
this.inlineAlert.showInlineConfirmation({
|
||||
message: "ALERT.FORM_CHANGE_CONFIRMATION"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.opened = false;
|
||||
}
|
||||
}
|
||||
|
||||
submit() {
|
||||
@ -92,12 +119,22 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
.then(() => {
|
||||
this.isOnCalling = false;
|
||||
this.close();
|
||||
this.msgService.announceMessage(200, "PROFILE.SAVE_SUCCESS", AlertType.SUCCESS);
|
||||
})
|
||||
.catch(error => {
|
||||
this.isOnCalling = false;
|
||||
this.error = error;
|
||||
this.alertClose = false;
|
||||
if(accessErrorHandler(error, this.msgService)){
|
||||
this.opened = false;
|
||||
}else{
|
||||
this.inlineAlert.showInlineError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
confirmCancel(): void {
|
||||
this.inlineAlert.close();
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,9 @@ import { SignInComponent } from './sign-in/sign-in.component';
|
||||
import { PasswordSettingComponent } from './password/password-setting.component';
|
||||
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
|
||||
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 { PasswordSettingService } from './password/password-setting.service';
|
||||
|
||||
@ -15,9 +18,19 @@ import { PasswordSettingService } from './password/password-setting.service';
|
||||
RouterModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent],
|
||||
exports: [SignInComponent, PasswordSettingComponent, AccountSettingsModalComponent],
|
||||
|
||||
declarations: [
|
||||
SignInComponent,
|
||||
PasswordSettingComponent,
|
||||
AccountSettingsModalComponent,
|
||||
SignUpComponent,
|
||||
ForgotPasswordComponent,
|
||||
ResetPasswordComponent],
|
||||
exports: [
|
||||
SignInComponent,
|
||||
PasswordSettingComponent,
|
||||
AccountSettingsModalComponent,
|
||||
ResetPasswordComponent],
|
||||
|
||||
providers: [PasswordSettingService]
|
||||
})
|
||||
export class AccountModule { }
|
@ -0,0 +1,32 @@
|
||||
<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;">
|
||||
<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>
|
||||
<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"
|
||||
(input)="handleValidation(true)"
|
||||
(focusout)="handleValidation(false)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.EMAIL' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</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>
|
||||
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="send()">{{'BUTTON.SEND' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -1,10 +1,78 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { PasswordSettingService } from './password-setting.service';
|
||||
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
|
||||
|
||||
@Component({
|
||||
selector: 'forgot-password',
|
||||
templateUrl: "forgot-password.component.html"
|
||||
templateUrl: "forgot-password.component.html",
|
||||
styleUrls: ['password.component.css']
|
||||
})
|
||||
export class ForgotPasswordComponent {
|
||||
// constructor(private router: Router){}
|
||||
opened: boolean = false;
|
||||
private onGoing: boolean = false;
|
||||
private email: string = "";
|
||||
private validationState: boolean = true;
|
||||
private forceValid: boolean = true;
|
||||
|
||||
@ViewChild("forgotPasswordFrom") forgotPwdForm: NgForm;
|
||||
@ViewChild(InlineAlertComponent)
|
||||
private inlineAlert: InlineAlertComponent;
|
||||
|
||||
constructor(private pwdService: PasswordSettingService) { }
|
||||
|
||||
public get showProgress(): boolean {
|
||||
return this.onGoing;
|
||||
}
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.forgotPwdForm && this.forgotPwdForm.valid && this.forceValid;
|
||||
}
|
||||
|
||||
public open(): void {
|
||||
this.opened = true;
|
||||
this.validationState = true;
|
||||
this.forceValid = true;
|
||||
this.forgotPwdForm.resetForm();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
public send(): void {
|
||||
//Double confirm to avoid improper situations
|
||||
if (!this.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onGoing = true;
|
||||
this.pwdService.sendResetPasswordMail(this.email)
|
||||
.then(response => {
|
||||
this.onGoing = false;
|
||||
this.forceValid = false;//diable the send button
|
||||
this.inlineAlert.showInlineSuccess({
|
||||
message: "RESET_PWD.SUCCESS"
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
this.inlineAlert.showInlineError(error);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public handleValidation(flag: boolean): void {
|
||||
if (flag) {
|
||||
this.validationState = true;
|
||||
} else {
|
||||
this.validationState = this.isValid;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,55 +1,56 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true">
|
||||
<h3 class="modal-title">Change Password</h3>
|
||||
<h3 class="modal-title">{{'CHANGE_PWD.TITLE' | translate}}</h3>
|
||||
<div class="modal-body" style="min-height: 250px; overflow-y: hidden;">
|
||||
<form #changepwdForm="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="oldPassword">Current Password</label>
|
||||
<label for="oldPassword">{{'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="Enter current password"
|
||||
<input type="password" id="oldPassword" placeholder='{{"PLACEHOLDER.CURRENT_PWD" | translate}}'
|
||||
required
|
||||
name="oldPassword"
|
||||
[(ngModel)]="oldPwd"
|
||||
#oldPassInput="ngModel" size="25">
|
||||
<span class="tooltip-content">
|
||||
Current password is Required.
|
||||
{{'TOOLTIP.CURRENT_PWD' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">New Password</label>
|
||||
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)">
|
||||
<input type="password" id="newPassword" placeholder="Enter new password"
|
||||
<label for="newPassword">{{'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">
|
||||
<span class="tooltip-content">
|
||||
Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number
|
||||
{{'TOOLTIP.PASSWORD' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reNewPassword">Confirm Password</label>
|
||||
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="(reNewPassInput.invalid && (reNewPassInput.dirty || reNewPassInput.touched)) || (!newPassInput.invalid && reNewPassInput.value != newPassInput.value)">
|
||||
<input type="password" id="reNewPassword" placeholder="Confirm new password"
|
||||
<label for="reNewPassword">{{'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">
|
||||
<span class="tooltip-content">
|
||||
Password should be at least 7 characters with 1 uppercase, 1 lowercase letter and 1 number and same with new password
|
||||
{{'TOOLTIP.CONFIRM_PWD' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<inline-alert (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
|
||||
<button type="button" class="btn btn-outline" (click)="close()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="doOk()">Ok</button>
|
||||
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="doOk()">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -4,6 +4,10 @@ import { NgForm } from '@angular/forms';
|
||||
|
||||
import { PasswordSettingService } from './password-setting.service';
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
import { AlertType, httpStatusCode } from '../../shared/shared.const';
|
||||
import { MessageService } from '../../global-message/message.service';
|
||||
import { errorHandler, isEmptyForm, accessErrorHandler } from '../../shared/shared.utils';
|
||||
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
|
||||
|
||||
@Component({
|
||||
selector: 'password-setting',
|
||||
@ -14,19 +18,27 @@ export class PasswordSettingComponent implements AfterViewChecked {
|
||||
oldPwd: string = "";
|
||||
newPwd: string = "";
|
||||
reNewPwd: string = "";
|
||||
error: any = null;
|
||||
|
||||
private formValueChanged: boolean = false;
|
||||
private onCalling: boolean = false;
|
||||
|
||||
pwdFormRef: NgForm;
|
||||
@ViewChild("changepwdForm") pwdForm: NgForm;
|
||||
constructor(private passwordService: PasswordSettingService, private session: SessionService){}
|
||||
@ViewChild(InlineAlertComponent)
|
||||
private inlineAlert: InlineAlertComponent;
|
||||
|
||||
constructor(
|
||||
private passwordService: PasswordSettingService,
|
||||
private session: SessionService,
|
||||
private msgService: MessageService) { }
|
||||
|
||||
//If form is valid
|
||||
public get isValid(): boolean {
|
||||
if (this.pwdForm && this.pwdForm.form.get("newPassword")) {
|
||||
return this.pwdForm.valid &&
|
||||
this.pwdForm.form.get("newPassword").value === this.pwdForm.form.get("reNewPassword").value;
|
||||
(this.pwdForm.form.get("newPassword").value === this.pwdForm.form.get("reNewPassword").value) &&
|
||||
this.error === null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -45,6 +57,8 @@ export class PasswordSettingComponent implements AfterViewChecked {
|
||||
if (this.pwdFormRef) {
|
||||
this.pwdFormRef.valueChanges.subscribe(data => {
|
||||
this.formValueChanged = true;
|
||||
this.error = null;
|
||||
this.inlineAlert.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -54,10 +68,26 @@ export class PasswordSettingComponent implements AfterViewChecked {
|
||||
open(): void {
|
||||
this.opened = true;
|
||||
this.pwdForm.reset();
|
||||
this.formValueChanged = false;
|
||||
}
|
||||
|
||||
//Close the moal dialog
|
||||
close(): void {
|
||||
if (this.formValueChanged) {
|
||||
if (isEmptyForm(this.pwdForm)) {
|
||||
this.opened = false;
|
||||
} else {
|
||||
//Need user confirmation
|
||||
this.inlineAlert.showInlineConfirmation({
|
||||
message: "ALERT.FORM_CHANGE_CONFIRMATION"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.opened = false;
|
||||
}
|
||||
}
|
||||
|
||||
confirmCancel(): void {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
@ -73,26 +103,31 @@ export class PasswordSettingComponent implements AfterViewChecked {
|
||||
|
||||
//Double confirm session is valid
|
||||
let cUser = this.session.getCurrentUser();
|
||||
if(!cUser){
|
||||
if (!cUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Call service
|
||||
this.onCalling = true;
|
||||
|
||||
this.passwordService.changePassword(cUser.user_id,
|
||||
{
|
||||
new_password: this.pwdForm.value.newPassword,
|
||||
old_password: this.pwdForm.value.oldPassword
|
||||
})
|
||||
.then(() => {
|
||||
this.onCalling = false;
|
||||
this.close();
|
||||
})
|
||||
.catch(error => {
|
||||
this.onCalling = false;
|
||||
console.error(error);//TODO:
|
||||
});
|
||||
//TODO:publish the successful message to general messae box
|
||||
this.passwordService.changePassword(cUser.user_id,
|
||||
{
|
||||
new_password: this.pwdForm.value.newPassword,
|
||||
old_password: this.pwdForm.value.oldPassword
|
||||
})
|
||||
.then(() => {
|
||||
this.onCalling = false;
|
||||
this.close();
|
||||
this.msgService.announceMessage(200, "CHANGE_PWD.SAVE_SUCCESS", AlertType.SUCCESS);
|
||||
})
|
||||
.catch(error => {
|
||||
this.onCalling = false;
|
||||
this.error = error;
|
||||
if(accessErrorHandler(error, this.msgService)){
|
||||
this.opened = false;
|
||||
}else{
|
||||
this.inlineAlert.showInlineError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@ import 'rxjs/add/operator/toPromise';
|
||||
import { PasswordSetting } from './password-setting';
|
||||
|
||||
const passwordChangeEndpoint = "/api/users/:user_id/password";
|
||||
const sendEmailEndpoint = "/sendEmail";
|
||||
const resetPasswordEndpoint = "/reset";
|
||||
|
||||
@Injectable()
|
||||
export class PasswordSettingService {
|
||||
@ -19,17 +21,46 @@ export class PasswordSettingService {
|
||||
constructor(private http: Http) { }
|
||||
|
||||
changePassword(userId: number, setting: PasswordSetting): Promise<any> {
|
||||
if(!setting || setting.new_password.trim()==="" || setting.old_password.trim()===""){
|
||||
if (!setting || setting.new_password.trim() === "" || setting.old_password.trim() === "") {
|
||||
return Promise.reject("Invalid data");
|
||||
}
|
||||
|
||||
let putUrl = passwordChangeEndpoint.replace(":user_id", userId+"");
|
||||
let putUrl = passwordChangeEndpoint.replace(":user_id", userId + "");
|
||||
return this.http.put(putUrl, JSON.stringify(setting), this.options)
|
||||
.toPromise()
|
||||
.then(() => null)
|
||||
.catch(error=>{
|
||||
return Promise.reject(error);
|
||||
});
|
||||
.toPromise()
|
||||
.then(() => null)
|
||||
.catch(error => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
sendResetPasswordMail(email: string): Promise<any> {
|
||||
if (!email) {
|
||||
return Promise.reject("Invalid email");
|
||||
}
|
||||
|
||||
let getUrl = sendEmailEndpoint + "?email=" + email;
|
||||
return this.http.get(getUrl, this.options).toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => {
|
||||
return Promise.reject(error);
|
||||
})
|
||||
}
|
||||
|
||||
resetPassword(uuid: string, newPassword: string): Promise<any> {
|
||||
if (!uuid || !newPassword) {
|
||||
return Promise.reject("Invalid reset uuid or password");
|
||||
}
|
||||
|
||||
return this.http.post(resetPasswordEndpoint, JSON.stringify({
|
||||
"reset_uuid": uuid,
|
||||
"password": newPassword
|
||||
}), this.options)
|
||||
.toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
.reset-modal-title-override {
|
||||
font-size: 14px !important;
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<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.CAPTION2' | translate}}</label>
|
||||
<div class="modal-body" style="overflow-y: hidden;">
|
||||
<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" 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
|
||||
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
|
||||
name="newPassword"
|
||||
[(ngModel)]="password"
|
||||
#newPassInput="ngModel"
|
||||
size="25"
|
||||
(input)='handleValidation("newPassword", true)'
|
||||
(focusout)='handleValidation("newPassword", false)'>
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.PASSWORD' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reNewPassword">{{'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
|
||||
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$"
|
||||
name="reNewPassword"
|
||||
[(ngModel)]="ngModel"
|
||||
#reNewPassInput
|
||||
size="25"
|
||||
(input)='handleValidation("reNewPassword", true)'
|
||||
(focusout)='handleValidation("reNewPassword", false)'>
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.CONFIRM_PWD' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<inline-alert></inline-alert>
|
||||
<div style="height: 30px;"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="spinner spinner-inline" style="top:8px;" [hidden]="showProgress === false"></span>
|
||||
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || showProgress" (click)="send()">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
123
src/ui_ng/src/app/account/password/reset-password.component.ts
Normal file
123
src/ui_ng/src/app/account/password/reset-password.component.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { Component, ViewChild, OnInit } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { PasswordSettingService } from './password-setting.service';
|
||||
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
|
||||
import { errorHandler, accessErrorHandler } from '../../shared/shared.utils';
|
||||
import { AlertType } from '../../shared/shared.const';
|
||||
import { MessageService } from '../../global-message/message.service';
|
||||
|
||||
@Component({
|
||||
selector: 'reset-password',
|
||||
templateUrl: "reset-password.component.html",
|
||||
styleUrls: ['password.component.css']
|
||||
})
|
||||
export class ResetPasswordComponent implements OnInit{
|
||||
opened: boolean = true;
|
||||
private onGoing: boolean = false;
|
||||
private password: string = "";
|
||||
private validationState: any = {};
|
||||
private resetUuid: string = "";
|
||||
private resetOk: boolean = false;
|
||||
|
||||
@ViewChild("resetPwdForm") resetPwdForm: NgForm;
|
||||
@ViewChild(InlineAlertComponent)
|
||||
private inlineAlert: InlineAlertComponent;
|
||||
|
||||
constructor(
|
||||
private pwdService: PasswordSettingService,
|
||||
private route: ActivatedRoute,
|
||||
private msgService: MessageService,
|
||||
private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.subscribe(params => this.resetUuid = params["reset_uuid"] || "");
|
||||
}
|
||||
|
||||
public get showProgress(): boolean {
|
||||
return this.onGoing;
|
||||
}
|
||||
|
||||
public get isValid(): boolean {
|
||||
return this.resetPwdForm && this.resetPwdForm.valid && this.samePassword();
|
||||
}
|
||||
|
||||
public getValidationState(key: string): boolean {
|
||||
return this.validationState && this.validationState[key];
|
||||
}
|
||||
|
||||
public open(): void {
|
||||
this.resetOk = false;
|
||||
this.opened = true;
|
||||
this.resetPwdForm.resetForm();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
public send(): void {
|
||||
//If already reset password ok, navigator to sign-in
|
||||
if(this.resetOk){
|
||||
this.router.navigate(['sign-in']);
|
||||
return;
|
||||
}
|
||||
|
||||
//Double confirm to avoid improper situations
|
||||
if (!this.password) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onGoing = true;
|
||||
this.pwdService.resetPassword(this.resetUuid, this.password)
|
||||
.then(() => {
|
||||
this.resetOk = true;
|
||||
this.inlineAlert.showInlineSuccess({message:'RESET_PWD.RESET_OK'});
|
||||
})
|
||||
.catch(error => {
|
||||
if(accessErrorHandler(error, this.msgService)){
|
||||
this.close();
|
||||
}else{
|
||||
this.inlineAlert.showInlineError(errorHandler(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public handleValidation(key: string, flag: boolean): void {
|
||||
if (flag) {
|
||||
if(!this.validationState[key]){
|
||||
this.validationState[key] = true;
|
||||
}
|
||||
} else {
|
||||
this.validationState[key] = this.getControlValidationState(key)
|
||||
}
|
||||
}
|
||||
|
||||
private getControlValidationState(key: string): boolean {
|
||||
if (this.resetPwdForm) {
|
||||
let control = this.resetPwdForm.controls[key];
|
||||
if (control) {
|
||||
return control.valid;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private samePassword(): boolean {
|
||||
if (this.resetPwdForm) {
|
||||
let control1 = this.resetPwdForm.controls["newPassword"];
|
||||
let control2 = this.resetPwdForm.controls["reNewPassword"];
|
||||
if (control1 && control2) {
|
||||
return control1.value == control2.value;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -4,4 +4,12 @@
|
||||
|
||||
.visibility-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
position: relative;
|
||||
line-height: 36px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
top: -5px;
|
||||
}
|
@ -7,32 +7,33 @@
|
||||
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="userNameInput.invalid && (userNameInput.dirty || userNameInput.touched)">
|
||||
<input class="username" type="text" required
|
||||
[(ngModel)]="signInCredential.principal"
|
||||
name="login_username" id="login_username" placeholder="Username"
|
||||
name="login_username" id="login_username" placeholder='{{"PLACEHOLDER.SIGN_IN_NAME" | translate}}'
|
||||
#userNameInput='ngModel'>
|
||||
<span class="tooltip-content">
|
||||
Username is required!
|
||||
{{ 'TOOLTIP.SIGN_IN_USERNAME' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
|
||||
<input class="password" type="password" required
|
||||
[(ngModel)]="signInCredential.password"
|
||||
name="login_password" id="login_password" placeholder="Password"
|
||||
name="login_password" id="login_password" placeholder='{{"PLACEHOLDER.SIGN_IN_PWD" | translate}}'
|
||||
#passwordInput="ngModel">
|
||||
<span class="tooltip-content">
|
||||
Password is required!
|
||||
{{ 'TOOLTIP.SIGN_IN_PWD' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" id="rememberme">
|
||||
<label for="rememberme">
|
||||
Remember me
|
||||
</label>
|
||||
<label for="rememberme">{{ 'SIGN_IN.REMEMBER' | translate }}</label>
|
||||
<a href="javascript:void(0)" class="forgot-password-link" (click)="forgotPassword()">Forgot password</a>
|
||||
</div>
|
||||
<div [class.visibility-hidden]="signInStatus != statusError" class="error active">
|
||||
Invalid user name or password
|
||||
<div [class.visibility-hidden]="!isError" class="error active">
|
||||
{{ 'SIGN_IN.INVALID_MSG' | translate }}
|
||||
</div>
|
||||
<button [disabled]="signInStatus === statusOnGoing" type="submit" class="btn btn-primary" (click)="signIn()">LOG IN</button>
|
||||
<a href="javascript:void(0)" class="signup" (click)="signUp()">Sign up for an account</a>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<sign-up #signupDialog></sign-up>>
|
||||
<forgot-password #forgotPwdDialog></forgot-password>
|
@ -1,11 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Input, ViewChild, AfterViewChecked } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
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 { ForgotPasswordComponent } from '../password/forgot-password.component';
|
||||
|
||||
//Define status flags for signing in states
|
||||
export const signInStatusNormal = 0;
|
||||
export const signInStatusOnGoing = 1;
|
||||
@ -17,13 +21,16 @@ export const signInStatusError = -1;
|
||||
styleUrls: ['sign-in.component.css']
|
||||
})
|
||||
|
||||
export class SignInComponent implements AfterViewChecked {
|
||||
export class SignInComponent implements AfterViewChecked, OnInit {
|
||||
private redirectUrl: string = "";
|
||||
//Form reference
|
||||
signInForm: NgForm;
|
||||
@ViewChild('signInForm') currentForm: NgForm;
|
||||
@ViewChild('signupDialog') signUpDialog: SignUpComponent;
|
||||
@ViewChild('forgotPwdDialog') forgotPwdDialog: ForgotPasswordComponent;
|
||||
|
||||
//Status flag
|
||||
signInStatus: number = 0;
|
||||
signInStatus: number = signInStatusNormal;
|
||||
|
||||
//Initialize sign in credential
|
||||
@Input() signInCredential: SignInCredential = {
|
||||
@ -33,22 +40,29 @@ export class SignInComponent implements AfterViewChecked {
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private session: SessionService
|
||||
private session: SessionService,
|
||||
private route: ActivatedRoute
|
||||
) { }
|
||||
|
||||
//For template accessing
|
||||
get statusError(): number {
|
||||
return signInStatusError;
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams
|
||||
.subscribe(params => {
|
||||
this.redirectUrl = params["redirect_url"] || "";
|
||||
});
|
||||
}
|
||||
|
||||
get statusOnGoing(): number {
|
||||
return signInStatusOnGoing;
|
||||
//For template accessing
|
||||
public get isError(): boolean {
|
||||
return this.signInStatus === signInStatusError;
|
||||
}
|
||||
|
||||
public get isOnGoing(): boolean {
|
||||
return this.signInStatus === signInStatusOnGoing;
|
||||
}
|
||||
|
||||
//Validate the related fields
|
||||
private validate(): boolean {
|
||||
return true;
|
||||
//return this.signInForm.valid;
|
||||
public get isValid(): boolean {
|
||||
return this.currentForm.form.valid;
|
||||
}
|
||||
|
||||
//General error handler
|
||||
@ -93,7 +107,7 @@ export class SignInComponent implements AfterViewChecked {
|
||||
//Trigger the signin action
|
||||
signIn(): void {
|
||||
//Should validate input firstly
|
||||
if (!this.validate() || this.signInStatus === signInStatusOnGoing) {
|
||||
if (!this.isValid || this.isOnGoing) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -106,25 +120,26 @@ export class SignInComponent implements AfterViewChecked {
|
||||
//Set status
|
||||
this.signInStatus = signInStatusNormal;
|
||||
|
||||
//Validate the sign-in session
|
||||
this.session.retrieveUser()
|
||||
.then(user => {
|
||||
//Routing to the right location
|
||||
let nextRoute = ["/harbor", "projects"];
|
||||
this.router.navigate(nextRoute);
|
||||
})
|
||||
.catch(error => {
|
||||
this.handleError(error);
|
||||
});
|
||||
//Redirect to the right route
|
||||
if (this.redirectUrl === "") {
|
||||
//Routing to the default location
|
||||
this.router.navigateByUrl(harborRootRoute);
|
||||
}else{
|
||||
this.router.navigateByUrl(this.redirectUrl);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
//Help user navigate to the sign up
|
||||
//Open sign up dialog
|
||||
signUp(): void {
|
||||
let nextRoute = ["/harbor", "signup"];
|
||||
this.router.navigate(nextRoute);
|
||||
this.signUpDialog.open();
|
||||
}
|
||||
|
||||
//Open forgot password dialog
|
||||
forgotPassword(): void {
|
||||
this.forgotPwdDialog.open();
|
||||
}
|
||||
}
|
@ -2,10 +2,9 @@ import { Injectable } from '@angular/core';
|
||||
import { Headers, Http, URLSearchParams } from '@angular/http';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
import { SignInCredential } from './sign-in-credential';
|
||||
import { SignInCredential } from '../../shared/sign-in-credential';
|
||||
|
||||
const url_prefix = '/ng';
|
||||
const signInUrl = url_prefix + '/login';
|
||||
const signInUrl = '/login';
|
||||
/**
|
||||
*
|
||||
* Define a service to provide sign in methods
|
||||
|
@ -1 +1,12 @@
|
||||
<p>Placeholder for signup</p>
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="true">
|
||||
<h3 class="modal-title">{{'SIGN_UP.TITLE' | translate}}</h3>
|
||||
<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>
|
||||
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid || inProgress" (click)="create()">{{ 'BUTTON.SIGN_UP' | translate }}</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -1,10 +1,108 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Component, Output, ViewChild } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { NewUserFormComponent } from '../../shared/new-user-form/new-user-form.component';
|
||||
import { User } from '../../user/user';
|
||||
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { errorHandler } from '../../shared/shared.utils';
|
||||
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
|
||||
|
||||
@Component({
|
||||
selector: 'sign-up',
|
||||
templateUrl: "sign-up.component.html"
|
||||
})
|
||||
export class SignUpComponent {
|
||||
// constructor(private router: Router){}
|
||||
opened: boolean = false;
|
||||
staticBackdrop: boolean = true;
|
||||
private error: any;
|
||||
private onGoing: boolean = false;
|
||||
private formValueChanged: boolean = false;
|
||||
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private userService: UserService) { }
|
||||
|
||||
@ViewChild(NewUserFormComponent)
|
||||
private newUserForm: NewUserFormComponent;
|
||||
|
||||
@ViewChild(InlineAlertComponent)
|
||||
private inlienAlert: InlineAlertComponent;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
formValueChange(flag: boolean): void {
|
||||
if (flag) {
|
||||
this.formValueChanged = true;
|
||||
}
|
||||
if (this.error != null) {
|
||||
this.error = null;//clear error
|
||||
}
|
||||
this.inlienAlert.close();//Close alert if being shown
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this.newUserForm.reset();//Reset form
|
||||
this.formValueChanged = false;
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.formValueChanged) {
|
||||
if (this.newUserForm.isEmpty()) {
|
||||
this.opened = false;
|
||||
} else {
|
||||
//Need user confirmation
|
||||
this.inlienAlert.showInlineConfirmation({
|
||||
message: "ALERT.FORM_CHANGE_CONFIRMATION"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.opened = false;
|
||||
}
|
||||
}
|
||||
|
||||
confirmCancel(): void {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
//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.close();
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
this.error = error;
|
||||
this.inlienAlert.showInlineError(error);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
// import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CookieService } from 'angular2-cookie/core';
|
||||
|
||||
import { supportedLangs, enLang } from './shared/shared.const';
|
||||
import { SessionService } from './shared/session.service';
|
||||
|
||||
@Component({
|
||||
selector: 'harbor-app',
|
||||
@ -7,6 +11,28 @@ import { Component } from '@angular/core';
|
||||
styleUrls: []
|
||||
})
|
||||
export class AppComponent {
|
||||
// constructor(private router: Router) {
|
||||
// }
|
||||
constructor(
|
||||
private translate: TranslateService,
|
||||
private cookie: CookieService,
|
||||
private session: SessionService) {
|
||||
translate.addLangs(supportedLangs);
|
||||
translate.setDefaultLang(enLang);
|
||||
|
||||
//If user has selected lang, then directly use it
|
||||
let langSetting = this.cookie.get("harbor-lang");
|
||||
if (!langSetting || langSetting.trim() === "") {
|
||||
//Use browser lang
|
||||
langSetting = translate.getBrowserLang();
|
||||
}
|
||||
|
||||
let selectedLang = this.isLangMatch(langSetting, supportedLangs) ? langSetting : enLang;
|
||||
translate.use(selectedLang);
|
||||
//this.session.switchLanguage(selectedLang).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
private isLangMatch(browserLang: string, supportedLangs: string[]) {
|
||||
if (supportedLangs && supportedLangs.length > 0) {
|
||||
return supportedLangs.find(lang => lang === browserLang);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,16 @@ import { BaseModule } from './base/base.module';
|
||||
import { HarborRoutingModule } from './harbor-routing.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { AccountModule } from './account/account.module';
|
||||
import { ConfigurationModule } from './config/config.module';
|
||||
|
||||
import { TranslateModule, TranslateLoader, MissingTranslationHandler } from "@ngx-translate/core";
|
||||
import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { Http } from '@angular/http';
|
||||
|
||||
export function HttpLoaderFactory(http: Http) {
|
||||
return new TranslateHttpLoader(http, 'ng/i18n/lang/', '-lang.json');
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -18,7 +28,19 @@ import { AccountModule } from './account/account.module';
|
||||
SharedModule,
|
||||
BaseModule,
|
||||
AccountModule,
|
||||
HarborRoutingModule
|
||||
HarborRoutingModule,
|
||||
ConfigurationModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: (HttpLoaderFactory),
|
||||
deps: [Http]
|
||||
},
|
||||
missingTranslationHandler: {
|
||||
provide: MissingTranslationHandler,
|
||||
useClass: MyMissingTranslationHandler
|
||||
}
|
||||
})
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Router, Resolve, ActivatedRouteSnapshot, RouterStateSnapshot
|
||||
} from '@angular/router';
|
||||
|
||||
import { SessionService } from '../shared/session.service';
|
||||
import { SessionUser } from '../shared/session-user';
|
||||
|
||||
@Injectable()
|
||||
export class BaseRoutingResolver implements Resolve<SessionUser> {
|
||||
|
||||
constructor(private session: SessionService, private router: Router) { }
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<SessionUser> {
|
||||
return this.session.retrieveUser()
|
||||
.then(sessionUser => {
|
||||
return sessionUser;
|
||||
})
|
||||
.catch(error => {
|
||||
console.info("Anonymous user");
|
||||
});
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
|
||||
|
||||
import { DashboardComponent } from '../dashboard/dashboard.component';
|
||||
import { ProjectComponent } from '../project/project.component';
|
||||
import { UserComponent } from '../user/user.component';
|
||||
|
||||
import { BaseRoutingResolver } from './base-routing-resolver.service';
|
||||
|
||||
const baseRoutes: Routes = [
|
||||
{
|
||||
path: 'harbor',
|
||||
component: HarborShellComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
component: ProjectComponent
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
component: UserComponent,
|
||||
resolve: {
|
||||
projectsResolver: BaseRoutingResolver
|
||||
}
|
||||
}
|
||||
]
|
||||
}];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(baseRoutes)
|
||||
],
|
||||
exports: [RouterModule],
|
||||
|
||||
providers: [BaseRoutingResolver]
|
||||
})
|
||||
export class BaseRoutingModule {
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { DashboardModule } from '../dashboard/dashboard.module';
|
||||
import { ProjectModule } from '../project/project.module';
|
||||
@ -12,16 +13,14 @@ import { FooterComponent } from './footer/footer.component';
|
||||
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
|
||||
import { SearchResultComponent } from './global-search/search-result.component';
|
||||
|
||||
import { BaseRoutingModule } from './base-routing.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
DashboardModule,
|
||||
ProjectModule,
|
||||
UserModule,
|
||||
BaseRoutingModule,
|
||||
AccountModule
|
||||
AccountModule,
|
||||
RouterModule
|
||||
],
|
||||
declarations: [
|
||||
NavigatorComponent,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<form class="search">
|
||||
<label for="search_input">
|
||||
<input #globalSearchBox id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder="Search Harbor...">
|
||||
<input #globalSearchBox id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder='{{"GLOBAL_SEARCH.PLACEHOLDER" | translate}}'>
|
||||
</label>
|
||||
</form>
|
@ -1,12 +1,13 @@
|
||||
.search-overlay {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 94%;
|
||||
height: 100%;
|
||||
width: 97%;
|
||||
/*shoud be lesser than 1000 to aoivd override the popup menu*/
|
||||
z-index: 999;
|
||||
box-sizing: border-box;
|
||||
background: #fafafa;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.search-header {
|
||||
|
@ -1,25 +1,24 @@
|
||||
<clr-main-container>
|
||||
<global-message [isAppLevel]="true"></global-message>
|
||||
<navigator (showAccountSettingsModal)="openModal($event)" (searchEvt)="doSearch($event)" (showPwdChangeModal)="openModal($event)"></navigator>
|
||||
<global-message></global-message>
|
||||
<div class="content-container">
|
||||
<div class="content-area" [class.container-override]="showSearch">
|
||||
<global-message [isAppLevel]="false"></global-message>
|
||||
<!-- Only appear when searching -->
|
||||
<search-result (closeEvt)="searchClose($event)"></search-result>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<nav class="sidenav" [class.side-nav-override]="showSearch">
|
||||
<nav class="sidenav" *ngIf="isUserExisting" [class.side-nav-override]="showSearch" (click)='watchClickEvt()'>
|
||||
<section class="sidenav-content">
|
||||
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">
|
||||
Projects
|
||||
</a>
|
||||
<section class="nav-group collapsible">
|
||||
<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">
|
||||
<input id="tabsystem" type="checkbox">
|
||||
<label for="tabsystem">System Managements</label>
|
||||
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label>
|
||||
<ul class="nav-list">
|
||||
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">Users</a></li>
|
||||
<li><a class="nav-link">Replications</a></li>
|
||||
<li><a class="nav-link">Quarantine[*]</a></li>
|
||||
<li><a class="nav-link">Configurations[*]</a></li>
|
||||
<li><a class="nav-link" routerLink="/harbor/users" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.USERS' | translate}}</a></li>
|
||||
<li><a class="nav-link" routerLink="/harbor/replications/endpoints" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATIONS' | translate}}</a></li>
|
||||
<li><a class="nav-link" routerLink="/harbor/configs" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.CONFIGS' | translate}}</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
@ -27,4 +26,6 @@
|
||||
</div>
|
||||
</clr-main-container>
|
||||
<account-settings-modal></account-settings-modal>
|
||||
<password-setting></password-setting>
|
||||
<password-setting></password-setting>
|
||||
<deletion-dialog></deletion-dialog>
|
||||
<about-dialog></about-dialog>
|
@ -3,12 +3,15 @@ import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ModalEvent } from '../modal-event';
|
||||
import { SearchEvent } from '../search-event';
|
||||
import { modalAccountSettings, modalPasswordSetting } from '../modal-events.const';
|
||||
import { modalEvents } from '../modal-events.const';
|
||||
|
||||
import { AccountSettingsModalComponent } from '../../account/account-settings/account-settings-modal.component';
|
||||
import { SearchResultComponent } from '../global-search/search-result.component';
|
||||
import { PasswordSettingComponent } from '../../account/password/password-setting.component';
|
||||
import { NavigatorComponent } from '../navigator/navigator.component';
|
||||
import { SessionService } from '../../shared/session.service';
|
||||
|
||||
import { AboutDialogComponent } from '../../shared/about-dialog/about-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'harbor-shell',
|
||||
@ -30,11 +33,16 @@ export class HarborShellComponent implements OnInit {
|
||||
@ViewChild(NavigatorComponent)
|
||||
private navigator: NavigatorComponent;
|
||||
|
||||
@ViewChild(AboutDialogComponent)
|
||||
private aboutDialog: AboutDialogComponent;
|
||||
|
||||
//To indicator whwther or not the search results page is displayed
|
||||
//We need to use this property to do some overriding work
|
||||
private isSearchResultsOpened: boolean = false;
|
||||
|
||||
constructor(private route: ActivatedRoute) { }
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private session: SessionService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe(data => {
|
||||
@ -46,15 +54,28 @@ export class HarborShellComponent implements OnInit {
|
||||
return this.isSearchResultsOpened;
|
||||
}
|
||||
|
||||
public get isSystemAdmin(): boolean {
|
||||
let account = this.session.getCurrentUser();
|
||||
return account != null && account.has_admin_role > 0;
|
||||
}
|
||||
|
||||
public get isUserExisting(): boolean {
|
||||
let account = this.session.getCurrentUser();
|
||||
return account != null;
|
||||
}
|
||||
|
||||
//Open modal dialog
|
||||
openModal(event: ModalEvent): void {
|
||||
switch (event.modalName) {
|
||||
case modalAccountSettings:
|
||||
case modalEvents.USER_PROFILE:
|
||||
this.accountSettingsModal.open();
|
||||
break;
|
||||
case modalPasswordSetting:
|
||||
case modalEvents.CHANGE_PWD:
|
||||
this.pwdSetting.open();
|
||||
break;
|
||||
case modalEvents.ABOUT:
|
||||
this.aboutDialog.open();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -77,4 +98,10 @@ export class HarborShellComponent implements OnInit {
|
||||
this.isSearchResultsOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
//Close serch result panel if existing
|
||||
watchClickEvt(): void {
|
||||
this.searchResultComponet.close();
|
||||
this.isSearchResultsOpened = false;
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { modalEvents } from './modal-events.const'
|
||||
|
||||
//Define a object to store the modal event
|
||||
export class ModalEvent {
|
||||
modalName: string;
|
||||
modalName: modalEvents;
|
||||
modalFlag: boolean; //true for open, false for close
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export const modalAccountSettings= "account-settings";
|
||||
export const modalPasswordSetting = "password-setting";
|
||||
export const enum modalEvents {
|
||||
USER_PROFILE, CHANGE_PWD, ABOUT
|
||||
}
|
@ -13,4 +13,8 @@
|
||||
padding: 2px 0px 2px 0px;
|
||||
vertical-align: middle;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lang-selected {
|
||||
font-weight: bold;
|
||||
}
|
@ -1,27 +1,21 @@
|
||||
<clr-header class="header-5 header">
|
||||
<div class="branding">
|
||||
<a href="#" class="nav-link">
|
||||
<a href="javascript:void(0)" class="nav-link" (click)="homeAction()">
|
||||
<clr-icon shape="vm-bug"></clr-icon>
|
||||
<span class="title">Harbor</span>
|
||||
</a>
|
||||
</div>
|
||||
<global-search (searchEvt)="transferSearchEvent($event)"></global-search>
|
||||
<div class="header-actions">
|
||||
<div *ngIf="!isSessionValid">
|
||||
<a href="javascript:void(0)" class="nav-link nav-text sign-in-override" routerLink="/sign-in" routerLinkActive="active">Sign In</a>
|
||||
<span class="custom-divider"></span>
|
||||
<a href="javascript:void(0)" class="nav-link nav-text sign-up-override" routerLink="/sign-up" routerLinkActive="active">Sign Up</a>
|
||||
</div>
|
||||
<clr-dropdown class="dropdown bottom-left">
|
||||
<button class="nav-icon" clrDropdownToggle style="width: 90px;">
|
||||
<clr-icon shape="world" style="left:-5px;"></clr-icon>
|
||||
<span>English</span>
|
||||
<clr-icon shape="world" style="left:-8px;"></clr-icon>
|
||||
<span>{{currentLang}}</span>
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem>English</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>中文简体</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>中文繁體</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("en")' [class.lang-selected]='matchLang("en")'>English</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)='switchLanguage("zh")' [class.lang-selected]='matchLang("zh")'>中文简体</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-right'" class="dropdown" *ngIf="isSessionValid">
|
||||
@ -31,12 +25,13 @@
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">User Profile</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">Change Password</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem>About</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openAccountSettingsModal()">{{'ACCOUNT_SETTINGS.PROFILE' | translate}}</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openChangePwdModal()">{{'ACCOUNT_SETTINGS.CHANGE_PWD' | translate}}</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="openAboutDialog()">{{'ACCOUNT_SETTINGS.ABOUT' | translate}}</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">Log out</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="logOut()">{{'ACCOUNT_SETTINGS.LOGOUT' | translate}}</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
<a href="javascript:void(0)" class="nav-link nav-text" (click)="openAboutDialog()" *ngIf="isSessionValid === false">{{'ACCOUNT_SETTINGS.ABOUT' | translate}}</a>
|
||||
</div>
|
||||
</clr-header>
|
@ -1,12 +1,16 @@
|
||||
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ModalEvent } from '../modal-event';
|
||||
import { SearchEvent } from '../search-event';
|
||||
import { modalAccountSettings, modalPasswordSetting } from '../modal-events.const';
|
||||
import { modalEvents } from '../modal-events.const';
|
||||
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'navigator',
|
||||
@ -21,11 +25,22 @@ export class NavigatorComponent implements OnInit {
|
||||
@Output() showPwdChangeModal = new EventEmitter<ModalEvent>();
|
||||
|
||||
private sessionUser: SessionUser = null;
|
||||
private selectedLang: string = enLang;
|
||||
|
||||
constructor(private session: SessionService, private router: Router) { }
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private router: Router,
|
||||
private translate: TranslateService,
|
||||
private cookie: CookieService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sessionUser = this.session.getCurrentUser();
|
||||
this.selectedLang = this.translate.currentLang;
|
||||
this.translate.onLangChange.subscribe(langChange => {
|
||||
this.selectedLang = langChange.lang;
|
||||
//Keep in cookie for next use
|
||||
this.cookie.put("harbor-lang", langChange.lang);
|
||||
});
|
||||
}
|
||||
|
||||
public get isSessionValid(): boolean {
|
||||
@ -33,13 +48,21 @@ export class NavigatorComponent implements OnInit {
|
||||
}
|
||||
|
||||
public get accountName(): string {
|
||||
return this.sessionUser?this.sessionUser.username: "";
|
||||
return this.sessionUser ? this.sessionUser.username : "";
|
||||
}
|
||||
|
||||
public get currentLang(): string {
|
||||
return languageNames[this.selectedLang];
|
||||
}
|
||||
|
||||
matchLang(lang: string): boolean {
|
||||
return lang.trim() === this.selectedLang;
|
||||
}
|
||||
|
||||
//Open the account setting dialog
|
||||
openAccountSettingsModal(): void {
|
||||
this.showAccountSettingsModal.emit({
|
||||
modalName: modalAccountSettings,
|
||||
modalName: modalEvents.USER_PROFILE,
|
||||
modalFlag: true
|
||||
});
|
||||
}
|
||||
@ -47,7 +70,15 @@ export class NavigatorComponent implements OnInit {
|
||||
//Open change password dialog
|
||||
openChangePwdModal(): void {
|
||||
this.showPwdChangeModal.emit({
|
||||
modalName: modalPasswordSetting,
|
||||
modalName: modalEvents.CHANGE_PWD,
|
||||
modalFlag: true
|
||||
});
|
||||
}
|
||||
|
||||
//Open about dialog
|
||||
openAboutDialog(): void {
|
||||
this.showPwdChangeModal.emit({
|
||||
modalName: modalEvents.ABOUT,
|
||||
modalFlag: true
|
||||
});
|
||||
}
|
||||
@ -67,4 +98,28 @@ export class NavigatorComponent implements OnInit {
|
||||
})
|
||||
.catch()//TODO:
|
||||
}
|
||||
|
||||
//Switch languages
|
||||
switchLanguage(lang: string): void {
|
||||
if (supportedLangs.find(supportedLang => supportedLang === lang.trim())){
|
||||
this.translate.use(lang);
|
||||
}else{
|
||||
this.translate.use(enLang);//Use default
|
||||
//TODO:
|
||||
console.error('Language '+lang.trim()+' is not suppoted');
|
||||
}
|
||||
//Try to switch backend lang
|
||||
//this.session.switchLanguage(lang).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
//Handle the home action
|
||||
homeAction(): void {
|
||||
if(this.sessionUser != null){
|
||||
//Navigate to default page
|
||||
this.router.navigate(['harbor','projects']);
|
||||
}else{
|
||||
//Naviagte to signin page
|
||||
this.router.navigate(['sign-in']);
|
||||
}
|
||||
}
|
||||
}
|
117
src/ui_ng/src/app/config/auth/config-auth.component.html
Normal file
117
src/ui_ng/src/app/config/auth/config-auth.component.html
Normal file
@ -0,0 +1,117 @@
|
||||
<form #authConfigFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<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">
|
||||
<option>db_auth</option>
|
||||
<option>ldap</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="form-block" *ngIf="showLdap">
|
||||
<div class="form-group">
|
||||
<label for="ldapUrl" class="required">LDAP URL</label>
|
||||
<label for="ldapUrl" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapUrlInput.invalid && (ldapUrlInput.dirty || ldapUrlInput.touched)">
|
||||
<input name="ldapUrl" type="text" #ldapUrlInput="ngModel" [(ngModel)]="currentConfig.ldap_url.value"
|
||||
required
|
||||
id="ldapUrl"
|
||||
size="40"
|
||||
[disabled]="disabled(currentConfig.ldap_url)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldapSearchDN" class="required">LDAP Search DN</label>
|
||||
<label for="ldapSearchDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapSearchDNInput.invalid && (ldapSearchDNInput.dirty || ldapSearchDNInput.touched)">
|
||||
<input name="ldapSearchDN" type="text" #ldapSearchDNInput="ngModel" [(ngModel)]="currentConfig.ldap_search_dn.value"
|
||||
required
|
||||
id="ldapSearchDN"
|
||||
size="40" [disabled]="disabled(currentConfig.ldap_search_dn)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldapSearchPwd" class="required">LDAP Search Password</label>
|
||||
<label for="ldapSearchPwd" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapSearchPwdInput.invalid && (ldapSearchPwdInput.dirty || ldapSearchPwdInput.touched)">
|
||||
<input name="ldapSearchPwd" type="password" #ldapSearchPwdInput="ngModel" [(ngModel)]="currentConfig.ldap_search_password.value"
|
||||
required
|
||||
id="ldapSearchPwd"
|
||||
size="40" [disabled]="disabled(currentConfig.ldap_search_password)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldapBaseDN" class="required">LDAP Base DN</label>
|
||||
<label for="ldapBaseDN" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapBaseDNInput.invalid && (ldapBaseDNInput.dirty || ldapBaseDNInput.touched)">
|
||||
<input name="ldapBaseDN" type="text" #ldapBaseDNInput="ngModel" [(ngModel)]="currentConfig.ldap_base_dn.value"
|
||||
required
|
||||
id="ldapBaseDN"
|
||||
size="40" [disabled]="disabled(currentConfig.ldap_base_dn)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldapFilter">LDAP Filter</label>
|
||||
<label for="ldapFilter" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right">
|
||||
<input name="ldapFilter" type="text" #ldapFilterInput="ngModel" [(ngModel)]="currentConfig.ldap_filter.value"
|
||||
id="ldapFilter"
|
||||
size="40" [disabled]="disabled(currentConfig.ldap_filter)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldapUid" class="required">LDAP UID</label>
|
||||
<label for="ldapUid" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="ldapUidInput.invalid && (ldapUidInput.dirty || ldapUidInput.touched)">
|
||||
<input name="ldapUid" type="text" #ldapUidInput="ngModel" [(ngModel)]="currentConfig.ldap_uid.value"
|
||||
required
|
||||
id="ldapUid"
|
||||
size="40" [disabled]="disabled(currentConfig.ldap_uid)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldapScope">lDAP Scope</label>
|
||||
<div class="select">
|
||||
<select id="ldapScope" name="ldapScope" [(ngModel)]="currentConfig.ldap_scope.value" [disabled]="disabled(currentConfig.ldap_scope)">
|
||||
<option value="1">Base</option>
|
||||
<option value="2">OneLevel</option>
|
||||
<option value="3">Subtree</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="proCreation">{{'CONFIG.PRO_CREATION_RESTRICTION' | translate}}</label>
|
||||
<div class="select">
|
||||
<select id="proCreation" name="proCreation" [(ngModel)]="currentConfig.project_creation_restriction.value" [disabled]="disabled(currentConfig.project_creation_restriction)">
|
||||
<option>everyone</option>
|
||||
<option>adminonly</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="selfReg">{{'CONFIG.SELF_REGISTRATION' | translate}}</label>
|
||||
<clr-checkbox name="selfReg" id="selfReg" [(ngModel)]="currentConfig.self_registration.value" [disabled]="disabled(currentConfig.self_registration)">
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-8px;">
|
||||
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.SELF_REGISTRATION_TOOLTIP' | translate}}</span>
|
||||
</a>
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
33
src/ui_ng/src/app/config/auth/config-auth.component.ts
Normal file
33
src/ui_ng/src/app/config/auth/config-auth.component.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { Configuration } from '../config';
|
||||
|
||||
@Component({
|
||||
selector: 'config-auth',
|
||||
templateUrl: "config-auth.component.html",
|
||||
styleUrls: ['../config.component.css']
|
||||
})
|
||||
export class ConfigurationAuthComponent {
|
||||
private changeSub: Subscription;
|
||||
@Input("ldapConfig") currentConfig: Configuration = new Configuration();
|
||||
|
||||
@ViewChild("authConfigFrom") authForm: NgForm;
|
||||
|
||||
constructor() { }
|
||||
|
||||
public get showLdap(): boolean {
|
||||
return this.currentConfig &&
|
||||
this.currentConfig.auth_mode &&
|
||||
this.currentConfig.auth_mode.value === 'ldap';
|
||||
}
|
||||
|
||||
private disabled(prop: any): boolean {
|
||||
return !(prop && prop.editable);
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this.authForm && this.authForm.valid;
|
||||
}
|
||||
}
|
0
src/ui_ng/src/app/config/config.component.css
Normal file
0
src/ui_ng/src/app/config/config.component.css
Normal file
54
src/ui_ng/src/app/config/config.component.html
Normal file
54
src/ui_ng/src/app/config/config.component.html
Normal file
@ -0,0 +1,54 @@
|
||||
<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>
|
||||
|
||||
<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.VERIFY_REMOTE_CERT_TOOLTIP' | 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]*$"
|
||||
id="tokenExpiration"
|
||||
size="40" [disabled]="disabled(allConfig.token_expiration)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.NUMBER_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</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>
|
||||
</div>
|
267
src/ui_ng/src/app/config/config.component.ts
Normal file
267
src/ui_ng/src/app/config/config.component.ts
Normal file
@ -0,0 +1,267 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
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 { errorHandler, accessErrorHandler } from '../shared/shared.utils';
|
||||
import { StringValueItem } from './config';
|
||||
import { DeletionDialogService } from '../shared/deletion-dialog/deletion-dialog.service';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { DeletionMessage } from '../shared/deletion-dialog/deletion-message'
|
||||
|
||||
import { ConfigurationAuthComponent } from './auth/config-auth.component';
|
||||
import { ConfigurationEmailComponent } from './email/config-email.component';
|
||||
|
||||
const fakePass = "fakepassword";
|
||||
|
||||
@Component({
|
||||
selector: 'config',
|
||||
templateUrl: "config.component.html",
|
||||
styleUrls: ['config.component.css']
|
||||
})
|
||||
export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
private onGoing: boolean = false;
|
||||
allConfig: Configuration = new Configuration();
|
||||
private currentTabId: string = "";
|
||||
private originalCopy: Configuration;
|
||||
private confirmSub: Subscription;
|
||||
|
||||
@ViewChild("repoConfigFrom") repoConfigForm: NgForm;
|
||||
@ViewChild("systemConfigFrom") systemConfigForm: NgForm;
|
||||
@ViewChild(ConfigurationEmailComponent) mailConfig: ConfigurationEmailComponent;
|
||||
@ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigurationService,
|
||||
private msgService: MessageService,
|
||||
private confirmService: DeletionDialogService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
//First load
|
||||
this.retrieveConfig();
|
||||
|
||||
this.confirmSub = this.confirmService.deletionConfirm$.subscribe(confirmation => {
|
||||
this.reset(confirmation.data);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.confirmSub) {
|
||||
this.confirmSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public get inProgress(): boolean {
|
||||
return this.onGoing;
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this.repoConfigForm &&
|
||||
this.repoConfigForm.valid &&
|
||||
this.systemConfigForm &&
|
||||
this.systemConfigForm.valid &&
|
||||
this.mailConfig &&
|
||||
this.mailConfig.isValid() &&
|
||||
this.authConfig &&
|
||||
this.authConfig.isValid();
|
||||
}
|
||||
|
||||
public hasChanges(): boolean {
|
||||
return !this.isEmpty(this.getChanges());
|
||||
}
|
||||
|
||||
public isMailConfigValid(): boolean {
|
||||
return this.mailConfig &&
|
||||
this.mailConfig.isValid();
|
||||
}
|
||||
|
||||
public get showTestServerBtn(): boolean {
|
||||
return this.currentTabId === 'config-email';
|
||||
}
|
||||
|
||||
public tabLinkChanged(tabLink: any) {
|
||||
this.currentTabId = tabLink.id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Save the changed values
|
||||
*
|
||||
* @memberOf ConfigurationComponent
|
||||
*/
|
||||
public save(): void {
|
||||
let changes = this.getChanges();
|
||||
if (!this.isEmpty(changes)) {
|
||||
this.onGoing = true;
|
||||
this.configService.saveConfiguration(changes)
|
||||
.then(response => {
|
||||
this.onGoing = false;
|
||||
//API should return the updated configurations here
|
||||
//Unfortunately API does not do that
|
||||
//To refresh the view, we can clone the original data copy
|
||||
//or force refresh by calling service.
|
||||
//HERE we choose force way
|
||||
this.retrieveConfig();
|
||||
this.msgService.announceMessage(response.status, "CONFIG.SAVE_SUCCESS", AlertType.SUCCESS);
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
if (!accessErrorHandler(error, this.msgService)) {
|
||||
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//Inprop situation, should not come here
|
||||
console.error("Save obort becasue nothing changed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Discard current changes if have and reset
|
||||
*
|
||||
* @memberOf ConfigurationComponent
|
||||
*/
|
||||
public cancel(): void {
|
||||
let changes = this.getChanges();
|
||||
if (!this.isEmpty(changes)) {
|
||||
let msg = new DeletionMessage(
|
||||
"CONFIG.CONFIRM_TITLE",
|
||||
"CONFIG.CONFIRM_SUMMARY",
|
||||
"",
|
||||
changes,
|
||||
DeletionTargets.EMPTY
|
||||
);
|
||||
this.confirmService.openComfirmDialog(msg);
|
||||
} else {
|
||||
//Inprop situation, should not come here
|
||||
console.error("Nothing changed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Test the connection of specified mail server
|
||||
*
|
||||
*
|
||||
* @memberOf ConfigurationComponent
|
||||
*/
|
||||
public testMailServer(): void {
|
||||
|
||||
}
|
||||
|
||||
private retrieveConfig(): void {
|
||||
this.onGoing = true;
|
||||
this.configService.getConfiguration()
|
||||
.then(configurations => {
|
||||
this.onGoing = false;
|
||||
|
||||
//Add two password fields
|
||||
configurations.email_password = new StringValueItem(fakePass, true);
|
||||
configurations.ldap_search_password = new StringValueItem(fakePass, true);
|
||||
this.allConfig = configurations;
|
||||
|
||||
//Keep the original copy of the data
|
||||
this.originalCopy = this.clone(configurations);
|
||||
})
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
if (!accessErrorHandler(error, this.msgService)) {
|
||||
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the changed fields and return a map
|
||||
*
|
||||
* @private
|
||||
* @returns {*}
|
||||
*
|
||||
* @memberOf ConfigurationComponent
|
||||
*/
|
||||
private getChanges(): any {
|
||||
let changes = {};
|
||||
if (!this.allConfig || !this.originalCopy) {
|
||||
return changes;
|
||||
}
|
||||
|
||||
for (let prop in this.allConfig) {
|
||||
let field = this.originalCopy[prop];
|
||||
if (field && field.editable) {
|
||||
if (field.value != this.allConfig[prop].value) {
|
||||
changes[prop] = this.allConfig[prop].value;
|
||||
//Fix boolean issue
|
||||
if (typeof field.value === "boolean") {
|
||||
changes[prop] = changes[prop] ? "1" : "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Deep clone the configuration object
|
||||
*
|
||||
* @private
|
||||
* @param {Configuration} src
|
||||
* @returns {Configuration}
|
||||
*
|
||||
* @memberOf ConfigurationComponent
|
||||
*/
|
||||
private clone(src: Configuration): Configuration {
|
||||
let dest = new Configuration();
|
||||
if (!src) {
|
||||
return dest;//Empty
|
||||
}
|
||||
|
||||
for (let prop in src) {
|
||||
if (src[prop]) {
|
||||
dest[prop] = Object.assign({}, src[prop]); //Deep copy inner object
|
||||
}
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Reset the configuration form
|
||||
*
|
||||
* @private
|
||||
* @param {*} changes
|
||||
*
|
||||
* @memberOf ConfigurationComponent
|
||||
*/
|
||||
private reset(changes: any): void {
|
||||
if (!this.isEmpty(changes)) {
|
||||
for (let prop in changes) {
|
||||
if (this.originalCopy[prop]) {
|
||||
this.allConfig[prop] = Object.assign({}, this.originalCopy[prop]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//force reset
|
||||
this.retrieveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private isEmpty(obj) {
|
||||
for (let key in obj) {
|
||||
if (obj.hasOwnProperty(key))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private disabled(prop: any): boolean {
|
||||
return !(prop && prop.editable);
|
||||
}
|
||||
}
|
22
src/ui_ng/src/app/config/config.module.ts
Normal file
22
src/ui_ng/src/app/config/config.module.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CoreModule } from '../core/core.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
import { ConfigurationComponent } from './config.component';
|
||||
import { ConfigurationService } from './config.service';
|
||||
import { ConfigurationAuthComponent } from './auth/config-auth.component';
|
||||
import { ConfigurationEmailComponent } from './email/config-email.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
ConfigurationComponent,
|
||||
ConfigurationAuthComponent,
|
||||
ConfigurationEmailComponent],
|
||||
exports: [ConfigurationComponent],
|
||||
providers: [ConfigurationService]
|
||||
})
|
||||
export class ConfigurationModule { }
|
33
src/ui_ng/src/app/config/config.service.ts
Normal file
33
src/ui_ng/src/app/config/config.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Headers, Http, RequestOptions } from '@angular/http';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
|
||||
import { Configuration } from './config';
|
||||
|
||||
const configEndpoint = "/api/configurations";
|
||||
|
||||
@Injectable()
|
||||
export class ConfigurationService {
|
||||
private headers: Headers = new Headers({
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/json'
|
||||
});
|
||||
private options: RequestOptions = new RequestOptions({
|
||||
'headers': this.headers
|
||||
});
|
||||
|
||||
constructor(private http: Http) { }
|
||||
|
||||
public getConfiguration(): Promise<Configuration> {
|
||||
return this.http.get(configEndpoint, this.options).toPromise()
|
||||
.then(response => response.json() as Configuration)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public saveConfiguration(values: any): Promise<any> {
|
||||
return this.http.put(configEndpoint, JSON.stringify(values), this.options)
|
||||
.toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
}
|
77
src/ui_ng/src/app/config/config.ts
Normal file
77
src/ui_ng/src/app/config/config.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export class StringValueItem {
|
||||
value: string;
|
||||
editable: boolean;
|
||||
|
||||
public constructor(v: string, e: boolean) {
|
||||
this.value = v;
|
||||
this.editable = e;
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberValueItem {
|
||||
value: number;
|
||||
editable: boolean;
|
||||
|
||||
public constructor(v: number, e: boolean) {
|
||||
this.value = v;
|
||||
this.editable = e;
|
||||
}
|
||||
}
|
||||
|
||||
export class BoolValueItem {
|
||||
value: boolean;
|
||||
editable: boolean;
|
||||
|
||||
public constructor(v: boolean, e: boolean) {
|
||||
this.value = v;
|
||||
this.editable = e;
|
||||
}
|
||||
}
|
||||
|
||||
export class Configuration {
|
||||
auth_mode: StringValueItem;
|
||||
project_creation_restriction: StringValueItem;
|
||||
self_registration: BoolValueItem;
|
||||
ldap_base_dn: StringValueItem;
|
||||
ldap_filter?: StringValueItem;
|
||||
ldap_scope: NumberValueItem;
|
||||
ldap_search_dn?: StringValueItem;
|
||||
ldap_search_password?: StringValueItem;
|
||||
ldap_timeout: NumberValueItem;
|
||||
ldap_uid: StringValueItem;
|
||||
ldap_url: StringValueItem;
|
||||
email_host: StringValueItem;
|
||||
email_identity: StringValueItem;
|
||||
email_from: StringValueItem;
|
||||
email_port: NumberValueItem;
|
||||
email_ssl: BoolValueItem;
|
||||
email_username?: StringValueItem;
|
||||
email_password?: StringValueItem;
|
||||
verify_remote_cert: BoolValueItem;
|
||||
token_expiration: NumberValueItem;
|
||||
cfg_expiration: NumberValueItem;
|
||||
|
||||
public constructor() {
|
||||
this.auth_mode = new StringValueItem("db_auth", true);
|
||||
this.project_creation_restriction = new StringValueItem("everyone", true);
|
||||
this.self_registration = new BoolValueItem(false, true);
|
||||
this.ldap_base_dn = new StringValueItem("", true);
|
||||
this.ldap_filter = new StringValueItem("", true);
|
||||
this.ldap_scope = new NumberValueItem(0, true);
|
||||
this.ldap_search_dn = new StringValueItem("", true);
|
||||
this.ldap_search_password = new StringValueItem("", true);
|
||||
this.ldap_timeout = new NumberValueItem(5, true);
|
||||
this.ldap_uid = new StringValueItem("", true);
|
||||
this.ldap_url = new StringValueItem("", true);
|
||||
this.email_host = new StringValueItem("", true);
|
||||
this.email_identity = new StringValueItem("", true);
|
||||
this.email_from = new StringValueItem("", true);
|
||||
this.email_port = new NumberValueItem(25, true);
|
||||
this.email_ssl = new BoolValueItem(false, true);
|
||||
this.email_username = new StringValueItem("", true);
|
||||
this.email_password = new StringValueItem("", true);
|
||||
this.token_expiration = new NumberValueItem(5, true);
|
||||
this.cfg_expiration = new NumberValueItem(30, true);
|
||||
this.verify_remote_cert = new BoolValueItem(false, true);
|
||||
}
|
||||
}
|
74
src/ui_ng/src/app/config/email/config-email.component.html
Normal file
74
src/ui_ng/src/app/config/email/config-email.component.html
Normal file
@ -0,0 +1,74 @@
|
||||
<form #mailConfigFrom="ngForm" class="form">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="mailServer" class="required">{{'CONFIG.MAIL_SERVER' | translate}}</label>
|
||||
<label for="mailServer" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="mailServerInput.invalid && (mailServerInput.dirty || mailServerInput.touched)">
|
||||
<input name="mailServer" type="text" #mailServerInput="ngModel" [(ngModel)]="currentConfig.email_host.value"
|
||||
required
|
||||
id="mailServer"
|
||||
size="40" [disabled]="disabled(currentConfig.email_host)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="emailPort" class="required">{{'CONFIG.MAIL_SERVER_PORT' | translate}}</label>
|
||||
<label for="emailPort" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="emailPortInput.invalid && (emailPortInput.dirty || emailPortInput.touched)">
|
||||
<input name="emailPort" type="text" #emailPortInput="ngModel" [(ngModel)]="currentConfig.email_port.value"
|
||||
required
|
||||
port
|
||||
id="emailPort"
|
||||
size="40" [disabled]="disabled(currentConfig.email_port)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.PORT_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="emailFrom" class="required">{{'CONFIG.MAIL_FROM' | translate}}</label>
|
||||
<label for="emailFrom" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="emailFromInput.invalid && (emailFromInput.dirty || emailFromInput.touched)">
|
||||
<input name="emailFrom" type="text" #emailFromInput="ngModel" [(ngModel)]="currentConfig.email_from.value"
|
||||
required
|
||||
id="emailFrom"
|
||||
size="40" [disabled]="disabled(currentConfig.email_from)">
|
||||
<span class="tooltip-content">
|
||||
{{'TOOLTIP.ITEM_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="selfReg">{{'CONFIG.MAIL_SSL' | translate}}</label>
|
||||
<clr-checkbox name="emailSSL" id="emailSSL" [(ngModel)]="currentConfig.email_ssl.value" [disabled]="disabled(currentConfig.email_ssl)">
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-8px;">
|
||||
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.SSL_TOOLTIP' | translate}}</span>
|
||||
</a>
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
25
src/ui_ng/src/app/config/email/config-email.component.ts
Normal file
25
src/ui_ng/src/app/config/email/config-email.component.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { Configuration } from '../config';
|
||||
|
||||
@Component({
|
||||
selector: 'config-email',
|
||||
templateUrl: "config-email.component.html",
|
||||
styleUrls: ['../config.component.css']
|
||||
})
|
||||
export class ConfigurationEmailComponent {
|
||||
@Input("mailConfig") currentConfig: Configuration = new Configuration();
|
||||
|
||||
@ViewChild("mailConfigFrom") mailForm: NgForm;
|
||||
|
||||
constructor() { }
|
||||
|
||||
private disabled(prop: any): boolean {
|
||||
return !(prop && prop.editable);
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return this.mailForm && this.mailForm.valid;
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
<clr-alert [clrAlertType]="'alert-danger'" [clrAlertAppLevel]="true" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()">
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">
|
||||
{{globalMessage}}
|
||||
</span>
|
||||
</div>
|
||||
<clr-alert [clrAlertType]="globalMessage.type" [clrAlertAppLevel]="isAppLevel" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()">
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">
|
||||
{{message}}
|
||||
</span>
|
||||
<div class="alert-actions" *ngIf="needAuth">
|
||||
<button class="btn alert-action" (click)="signIn()">{{ 'BUTTON.LOG_IN' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-alert>
|
@ -1,23 +1,100 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { Message } from './message';
|
||||
import { MessageService } from './message.service';
|
||||
|
||||
import { AlertType, dismissInterval, httpStatusCode } from '../shared/shared.const';
|
||||
|
||||
@Component({
|
||||
selector: 'global-message',
|
||||
templateUrl: 'message.component.html'
|
||||
})
|
||||
export class MessageComponent {
|
||||
|
||||
globalMessageOpened: boolean;
|
||||
globalMessage: string;
|
||||
export class MessageComponent implements OnInit {
|
||||
|
||||
constructor(messageService: MessageService) {
|
||||
messageService.messageAnnounced$.subscribe(
|
||||
message=>{
|
||||
this.globalMessageOpened = true;
|
||||
this.globalMessage = message;
|
||||
console.log('received message:' + message);
|
||||
@Input() isAppLevel: boolean;
|
||||
globalMessage: Message = new Message();
|
||||
globalMessageOpened: boolean;
|
||||
messageText: string = "";
|
||||
|
||||
constructor(
|
||||
private messageService: MessageService,
|
||||
private router: Router,
|
||||
private translate: TranslateService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
//Only subscribe application level message
|
||||
if (this.isAppLevel) {
|
||||
this.messageService.appLevelAnnounced$.subscribe(
|
||||
message => {
|
||||
this.globalMessageOpened = true;
|
||||
this.globalMessage = message;
|
||||
this.messageText = message.message;
|
||||
|
||||
this.translateMessage(message);
|
||||
}
|
||||
)
|
||||
} else {
|
||||
//Only subscribe general messages
|
||||
this.messageService.messageAnnounced$.subscribe(
|
||||
message => {
|
||||
this.globalMessageOpened = true;
|
||||
this.globalMessage = message;
|
||||
this.messageText = message.message;
|
||||
|
||||
this.translateMessage(message);
|
||||
|
||||
// Make the message alert bar dismiss after several intervals.
|
||||
//Only for this case
|
||||
setInterval(() => this.onClose(), dismissInterval);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//Translate or refactor the message shown to user
|
||||
translateMessage(msg: Message): void {
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
let key = "";
|
||||
if (!msg.message) {
|
||||
key = "UNKNOWN_ERROR";
|
||||
} else {
|
||||
key = typeof msg.message === "string" ? msg.message.trim() : msg.message;
|
||||
if (key === "") {
|
||||
key = "UNKNOWN_ERROR";
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
//Override key for HTTP 401 and 403
|
||||
if (this.globalMessage.statusCode === httpStatusCode.Unauthorized) {
|
||||
key = "UNAUTHORIZED_ERROR";
|
||||
}
|
||||
|
||||
if (this.globalMessage.statusCode === httpStatusCode.Forbidden) {
|
||||
key = "FORBIDDEN_ERROR";
|
||||
}
|
||||
|
||||
this.translate.get(key).subscribe((res: string) => this.messageText = res);
|
||||
}
|
||||
|
||||
public get needAuth(): boolean {
|
||||
return this.globalMessage ?
|
||||
(this.globalMessage.statusCode === httpStatusCode.Unauthorized) ||
|
||||
(this.globalMessage.statusCode === httpStatusCode.Forbidden) : false;
|
||||
}
|
||||
|
||||
//Show message text
|
||||
public get message(): string {
|
||||
return this.messageText;
|
||||
}
|
||||
|
||||
signIn(): void {
|
||||
this.router.navigate(['sign-in']);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
|
@ -1,14 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { Message } from './message';
|
||||
import { AlertType } from '../shared/shared.const';
|
||||
|
||||
@Injectable()
|
||||
export class MessageService {
|
||||
|
||||
private messageAnnouncedSource = new Subject<string>();
|
||||
private messageAnnouncedSource = new Subject<Message>();
|
||||
private appLevelAnnouncedSource = new Subject<Message>();
|
||||
|
||||
messageAnnounced$ = this.messageAnnouncedSource.asObservable();
|
||||
appLevelAnnounced$ = this.appLevelAnnouncedSource.asObservable();
|
||||
|
||||
announceMessage(statusCode: number, message: string, alertType: AlertType) {
|
||||
this.messageAnnouncedSource.next(Message.newMessage(statusCode, message, alertType));
|
||||
}
|
||||
|
||||
announceMessage(message: string) {
|
||||
this.messageAnnouncedSource.next(message);
|
||||
announceAppLevelMessage(statusCode: number, message: string, alertType: AlertType) {
|
||||
this.appLevelAnnouncedSource.next(Message.newMessage(statusCode, message, alertType));
|
||||
}
|
||||
}
|
40
src/ui_ng/src/app/global-message/message.ts
Normal file
40
src/ui_ng/src/app/global-message/message.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { AlertType } from '../shared/shared.const';
|
||||
|
||||
export class Message {
|
||||
statusCode: number;
|
||||
message: string;
|
||||
alertType: AlertType;
|
||||
isAppLevel: boolean = false;
|
||||
|
||||
get type(): string {
|
||||
switch (this.alertType) {
|
||||
case AlertType.DANGER:
|
||||
return 'alert-danger';
|
||||
case AlertType.INFO:
|
||||
return 'alert-info';
|
||||
case AlertType.SUCCESS:
|
||||
return 'alert-success';
|
||||
case AlertType.WARNING:
|
||||
return 'alert-warning';
|
||||
default:
|
||||
return 'alert-warning';
|
||||
}
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
|
||||
static newMessage(statusCode: number, message: string, alertType: AlertType): Message {
|
||||
let m = new Message();
|
||||
m.statusCode = statusCode;
|
||||
m.message = message;
|
||||
m.alertType = alertType;
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
toString(): string {
|
||||
return 'Message with statusCode:' + this.statusCode +
|
||||
', message:' + this.message +
|
||||
', alert type:' + this.type;
|
||||
}
|
||||
}
|
@ -4,16 +4,101 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { SignInComponent } from './account/sign-in/sign-in.component';
|
||||
import { HarborShellComponent } from './base/harbor-shell/harbor-shell.component';
|
||||
import { ProjectComponent } from './project/project.component';
|
||||
import { UserComponent } from './user/user.component';
|
||||
import { ReplicationManagementComponent } from './replication/replication-management/replication-management.component';
|
||||
|
||||
import { BaseRoutingResolver } from './base/base-routing-resolver.service';
|
||||
import { TotalReplicationComponent } from './replication/total-replication/total-replication.component';
|
||||
import { DestinationComponent } from './replication/destination/destination.component';
|
||||
|
||||
import { ProjectDetailComponent } from './project/project-detail/project-detail.component';
|
||||
|
||||
import { RepositoryComponent } from './repository/repository.component';
|
||||
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';
|
||||
import { ResetPasswordComponent } from './account/password/reset-password.component';
|
||||
import { RecentLogComponent } from './log/recent-log.component';
|
||||
import { ConfigurationComponent } from './config/config.component';
|
||||
import { PageNotFoundComponent } from './shared/not-found/not-found.component'
|
||||
|
||||
const harborRoutes: Routes = [
|
||||
{ path: '', redirectTo: '/harbor', pathMatch: 'full' },
|
||||
{ path: 'sign-in', component: SignInComponent },
|
||||
{ path: 'sign-up', component: SignUpComponent},
|
||||
{ path: 'reset_password', component: ResetPasswordComponent},
|
||||
{
|
||||
path: 'harbor',
|
||||
component: HarborShellComponent
|
||||
component: HarborShellComponent,
|
||||
resolve: {
|
||||
authResolver: BaseRoutingResolver
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'projects',
|
||||
component: ProjectComponent
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
component: RecentLogComponent
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
component: UserComponent,
|
||||
canActivate: [SystemAdminGuard]
|
||||
},
|
||||
{
|
||||
path: 'replications',
|
||||
component: ReplicationManagementComponent,
|
||||
canActivate: [SystemAdminGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'rules',
|
||||
component: TotalReplicationComponent
|
||||
},
|
||||
{
|
||||
path: 'endpoints',
|
||||
component: DestinationComponent
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'projects/:id',
|
||||
component: ProjectDetailComponent,
|
||||
resolve: {
|
||||
projectResolver: ProjectRoutingResolver
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'repository',
|
||||
component: RepositoryComponent
|
||||
},
|
||||
{
|
||||
path: 'replication',
|
||||
component: ReplicationComponent
|
||||
},
|
||||
{
|
||||
path: 'member',
|
||||
component: MemberComponent
|
||||
},
|
||||
{
|
||||
path: 'log',
|
||||
component: AuditLogComponent
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'configs',
|
||||
component: ConfigurationComponent
|
||||
}
|
||||
]
|
||||
},
|
||||
{ path: '', redirectTo: '/harbor', pathMatch: 'full' },
|
||||
{ path: 'sign-in', component: SignInComponent }
|
||||
{ path: "**", component: PageNotFoundComponent}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
8
src/ui_ng/src/app/i18n/missing-trans.handler.ts
Normal file
8
src/ui_ng/src/app/i18n/missing-trans.handler.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { MissingTranslationHandler, MissingTranslationHandlerParams } from '@ngx-translate/core';
|
||||
|
||||
export class MyMissingTranslationHandler implements MissingTranslationHandler {
|
||||
handle(params: MissingTranslationHandlerParams) {
|
||||
const missingText = "{Miss Harbor Text}";
|
||||
return params.key || missingText;
|
||||
}
|
||||
}
|
@ -2,21 +2,22 @@
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-right">
|
||||
<div class="col-xs-3 push-md-2 flex-xs-middle">
|
||||
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption]}}</button>
|
||||
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
|
||||
</div>
|
||||
<div class="col-xs-3 flex-xs-middle">
|
||||
<clr-icon shape="filter" style="position: relative; left: 15px;"></clr-icon><input style="padding-left: 20px;" type="text" placeholder="Filter logs" #searchUsername (keyup.enter)="doSearchAuditLogs(searchUsername.value)">
|
||||
<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 advance-option" [hidden]="currentOption === 0">
|
||||
<div class="col-xs-2 push-md-1">
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
|
||||
<button class="btn btn-link" clrDropdownToggle>
|
||||
All Operations
|
||||
{{'AUDIT_LOG.ALL_OPERATIONS' | translate}}
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)"><clr-icon shape="check" [hidden]="!f.checked"></clr-icon> {{f.description}}</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)"><clr-icon shape="check" [hidden]="!f.checked"></clr-icon> {{f.description | translate}}</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
@ -26,11 +27,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Username</clr-dg-column>
|
||||
<clr-dg-column>Repository Name</clr-dg-column>
|
||||
<clr-dg-column>Tag</clr-dg-column>
|
||||
<clr-dg-column>Operation</clr-dg-column>
|
||||
<clr-dg-column>Timestamp</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let l of auditLogs">
|
||||
<clr-dg-cell>{{l.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
|
||||
@ -38,7 +39,7 @@
|
||||
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.op_time}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (auditLogs ? auditLogs.length : 0) }} item(s)</clr-dg-footer>
|
||||
<clr-dg-footer>{{ (auditLogs ? auditLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
@ -7,8 +7,9 @@ import { SessionUser } from '../shared/session-user';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { SessionService } from '../shared/session.service';
|
||||
import { MessageService } from '../global-message/message.service';
|
||||
import { AlertType } from '../shared/shared.const';
|
||||
|
||||
export const optionalSearch: {} = {0: 'Advanced', 1: 'Simple'};
|
||||
export const optionalSearch: {} = {0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE'};
|
||||
|
||||
|
||||
export class FilterOption {
|
||||
@ -42,12 +43,12 @@ export class AuditLogComponent implements OnInit {
|
||||
toggleName = optionalSearch;
|
||||
currentOption: number = 0;
|
||||
filterOptions: FilterOption[] = [
|
||||
new FilterOption('all', 'All Operations', true),
|
||||
new FilterOption('pull', 'Pull', true),
|
||||
new FilterOption('push', 'Push', true),
|
||||
new FilterOption('create', 'Create', true),
|
||||
new FilterOption('delete', 'Delete', true),
|
||||
new FilterOption('others', 'Others', true)
|
||||
new FilterOption('all', 'AUDIT_LOG.ALL_OPERATIONS', true),
|
||||
new FilterOption('pull', 'AUDIT_LOG.PULL', true),
|
||||
new FilterOption('push', 'AUDIT_LOG.PUSH', true),
|
||||
new FilterOption('create', 'AUDIT_LOG.CREATE', true),
|
||||
new FilterOption('delete', 'AUDIT_LOG.DELETE', true),
|
||||
new FilterOption('others', 'AUDIT_LOG.OTHERS', true)
|
||||
];
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private auditLogService: AuditLogService, private messageService: MessageService) {
|
||||
@ -69,7 +70,7 @@ export class AuditLogComponent implements OnInit {
|
||||
response=>this.auditLogs = response,
|
||||
error=>{
|
||||
this.router.navigate(['/harbor', 'projects']);
|
||||
this.messageService.announceMessage('Failed to list audit logs with project ID:' + queryParam.project_id);
|
||||
this.messageService.announceMessage(error.status, 'Failed to list audit logs with project ID:' + queryParam.project_id, AlertType.DANGER);
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -135,4 +136,7 @@ export class AuditLogComponent implements OnInit {
|
||||
}
|
||||
this.doSearchByOptions();
|
||||
}
|
||||
refresh(): void {
|
||||
this.retrieve(this.queryParam);
|
||||
}
|
||||
}
|
@ -10,26 +10,39 @@ import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/observable/throw';
|
||||
|
||||
export const urlPrefix = '';
|
||||
export const logEndpoint = "/api/logs";
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogService extends BaseService {
|
||||
|
||||
private httpOptions = new RequestOptions({
|
||||
headers: new Headers({
|
||||
"Content-Type": 'application/json',
|
||||
"Accept": 'application/json'
|
||||
})
|
||||
});
|
||||
|
||||
constructor(private http: Http) {
|
||||
super();
|
||||
}
|
||||
|
||||
listAuditLogs(queryParam: AuditLog): Observable<AuditLog[]> {
|
||||
return this.http
|
||||
.post(urlPrefix + `/api/projects/${queryParam.project_id}/logs/filter`, {
|
||||
begin_timestamp: queryParam.begin_timestamp,
|
||||
end_timestamp: queryParam.end_timestamp,
|
||||
keywords: queryParam.keywords,
|
||||
operation: queryParam.operation,
|
||||
project_id: queryParam.project_id,
|
||||
username: queryParam.username })
|
||||
.map(response=>response.json() as AuditLog[])
|
||||
.catch(error=>this.handleError(error));
|
||||
.post(`/api/projects/${queryParam.project_id}/logs/filter`, {
|
||||
begin_timestamp: queryParam.begin_timestamp,
|
||||
end_timestamp: queryParam.end_timestamp,
|
||||
keywords: queryParam.keywords,
|
||||
operation: queryParam.operation,
|
||||
project_id: queryParam.project_id,
|
||||
username: queryParam.username
|
||||
})
|
||||
.map(response => response.json() as AuditLog[])
|
||||
.catch(error => this.handleError(error));
|
||||
}
|
||||
|
||||
getRecentLogs(lines: number): Observable<AuditLog[]> {
|
||||
return this.http.get(logEndpoint + "?lines=" + lines, this.httpOptions)
|
||||
.map(response => response.json() as AuditLog[])
|
||||
.catch(error => this.handleError(error));
|
||||
}
|
||||
|
||||
}
|
@ -2,10 +2,16 @@ import { NgModule } from '@angular/core';
|
||||
import { AuditLogComponent } from './audit-log.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { RecentLogComponent } from './recent-log.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [ SharedModule ],
|
||||
declarations: [ AuditLogComponent ],
|
||||
providers: [ AuditLogService ],
|
||||
exports: [ AuditLogComponent ]
|
||||
imports: [SharedModule],
|
||||
declarations: [
|
||||
AuditLogComponent,
|
||||
RecentLogComponent],
|
||||
providers: [AuditLogService],
|
||||
exports: [
|
||||
AuditLogComponent,
|
||||
RecentLogComponent]
|
||||
})
|
||||
export class LogModule {}
|
||||
export class LogModule { }
|
32
src/ui_ng/src/app/log/recent-log.component.css
Normal file
32
src/ui_ng/src/app/log/recent-log.component.css
Normal file
@ -0,0 +1,32 @@
|
||||
.h2-log-override {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.filter-log {
|
||||
float: right;
|
||||
margin-right: 24px;
|
||||
position: relative;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.action-head-pos {
|
||||
position: relative;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-lines-button {
|
||||
padding: 0px !important;
|
||||
min-width: 25px !important;
|
||||
}
|
||||
|
||||
.lines-button-toggole {
|
||||
font-size: 16px;
|
||||
text-decoration: underline;
|
||||
}
|
36
src/ui_ng/src/app/log/recent-log.component.html
Normal file
36
src/ui_ng/src/app/log/recent-log.component.html
Normal file
@ -0,0 +1,36 @@
|
||||
<div>
|
||||
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}</h2>
|
||||
<div class="action-head-pos">
|
||||
<span>
|
||||
<label>{{'RECENT_LOG.SUB_TITLE' | translate}} </label>
|
||||
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 10" (click)="setLines(10)">10</button>
|
||||
<label> | </label>
|
||||
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 25" (click)="setLines(25)">25</button>
|
||||
<label> | </label>
|
||||
<button type="submit" class="btn btn-link custom-lines-button" [class.lines-button-toggole]="lines === 50" (click)="setLines(50)">50</button>
|
||||
<label>{{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</label>
|
||||
</span>
|
||||
<grid-filter class="filter-log" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)"></grid-filter>
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let l of recentLogs">
|
||||
<clr-dg-cell>{{l.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{formatDateTime(l.op_time)}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
96
src/ui_ng/src/app/log/recent-log.component.ts
Normal file
96
src/ui_ng/src/app/log/recent-log.component.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AuditLog } from './audit-log';
|
||||
import { SessionUser } from '../shared/session-user';
|
||||
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { SessionService } from '../shared/session.service';
|
||||
import { MessageService } from '../global-message/message.service';
|
||||
import { AlertType } from '../shared/shared.const';
|
||||
import { errorHandler, accessErrorHandler } from '../shared/shared.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'recent-log',
|
||||
templateUrl: './recent-log.component.html',
|
||||
styleUrls: ['recent-log.component.css']
|
||||
})
|
||||
|
||||
export class RecentLogComponent implements OnInit {
|
||||
private sessionUser: SessionUser = null;
|
||||
private recentLogs: AuditLog[];
|
||||
private logsCache: AuditLog[];
|
||||
private onGoing: boolean = false;
|
||||
private lines: number = 10; //Support 10, 25 and 50
|
||||
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private msgService: MessageService,
|
||||
private logService: AuditLogService) {
|
||||
this.sessionUser = this.session.getCurrentUser();//Initialize session
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.retrieveLogs();
|
||||
}
|
||||
|
||||
public get inProgress(): boolean {
|
||||
return this.onGoing;
|
||||
}
|
||||
|
||||
public setLines(lines: number): void {
|
||||
this.lines = lines;
|
||||
if (this.lines < 10) {
|
||||
this.lines = 10;
|
||||
}
|
||||
|
||||
this.retrieveLogs();
|
||||
}
|
||||
|
||||
public doFilter(terms: string): void {
|
||||
if (terms.trim() === "") {
|
||||
this.recentLogs = this.logsCache.filter(log => log.username != "");
|
||||
return;
|
||||
}
|
||||
|
||||
this.recentLogs = this.logsCache.filter(log => this.isMatched(terms, log));
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.retrieveLogs();
|
||||
}
|
||||
|
||||
public formatDateTime(dateTime: string){
|
||||
let dt: Date = new Date(dateTime);
|
||||
return dt.toLocaleString();
|
||||
}
|
||||
|
||||
private retrieveLogs(): void {
|
||||
if (this.lines < 10) {
|
||||
this.lines = 10;
|
||||
}
|
||||
|
||||
this.onGoing = true;
|
||||
this.logService.getRecentLogs(this.lines)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.onGoing = false;
|
||||
this.logsCache = response; //Keep the data
|
||||
this.recentLogs = this.logsCache.filter(log => log.username != "");//To display
|
||||
},
|
||||
error => {
|
||||
this.onGoing = false;
|
||||
if (!accessErrorHandler(error, this.msgService)) {
|
||||
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private isMatched(terms: string, log: AuditLog): boolean {
|
||||
let reg = new RegExp('.*' + terms + '.*', 'i');
|
||||
return reg.test(log.username) ||
|
||||
reg.test(log.repo_name) ||
|
||||
reg.test(log.operation);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-right'" [clrCloseMenuOnItemClick]="true">
|
||||
<button clrDropdownToggle>
|
||||
<clr-icon shape="ellipses-vertical"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem>New Policy</a>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="toggle()">Make {{project.public === 0 ? 'Public' : 'Private'}} </a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="javascript:void(0)" clrDropdownItem (click)="delete()">Delete</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
@ -1,30 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Project } from '../project';
|
||||
import { ProjectService } from '../project.service';
|
||||
|
||||
@Component({
|
||||
selector: 'action-project',
|
||||
templateUrl: 'action-project.component.html'
|
||||
})
|
||||
export class ActionProjectComponent {
|
||||
|
||||
@Output() togglePublic = new EventEmitter<Project>();
|
||||
@Output() deleteProject = new EventEmitter<Project>();
|
||||
|
||||
@Input() project: Project;
|
||||
|
||||
constructor(private projectService: ProjectService) {}
|
||||
|
||||
toggle() {
|
||||
if(this.project) {
|
||||
this.project.public === 0 ? this.project.public = 1 : this.project.public = 0;
|
||||
this.togglePublic.emit(this.project);
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
if(this.project) {
|
||||
this.deleteProject.emit(this.project);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user