diff --git a/src/ui/auth/ldap/ldap.go b/src/ui/auth/ldap/ldap.go index 235013c3d..6b8942176 100644 --- a/src/ui/auth/ldap/ldap.go +++ b/src/ui/auth/ldap/ldap.go @@ -36,6 +36,10 @@ const metaChars = "&|!=~*<>()" func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { p := m.Principal + if len(strings.TrimSpace(p)) == 0 { + log.Debugf("LDAP authentication failed for empty user id.") + return nil, nil + } for _, c := range metaChars { if strings.ContainsRune(p, c) { return nil, fmt.Errorf("the principal contains meta char: %q", c) diff --git a/src/ui/auth/ldap/ldap_test.go b/src/ui/auth/ldap/ldap_test.go index e979c9d11..6563f4f68 100644 --- a/src/ui/auth/ldap/ldap_test.go +++ b/src/ui/auth/ldap/ldap_test.go @@ -131,4 +131,13 @@ func TestAuthenticate(t *testing.T) { if user != nil { t.Errorf("Nil user expected for wrong password") } + person.Principal = "" + person.Password = "" + user, err = auth.Authenticate(person) + if err != nil { + t.Errorf("unexpected ldap error: %v", err) + } + if user != nil { + t.Errorf("Nil user for empty credentials") + } } diff --git a/src/ui_ng/lib/src/repository/repository.component.spec.ts b/src/ui_ng/lib/src/repository/repository.component.spec.ts index 8793c6531..5b2a6c551 100644 --- a/src/ui_ng/lib/src/repository/repository.component.spec.ts +++ b/src/ui_ng/lib/src/repository/repository.component.spec.ts @@ -96,13 +96,14 @@ describe('RepositoryComponent (inline template)', ()=> { fixture.detectChanges(); comp.doSearchRepoNames('nginx'); fixture.detectChanges(); - let de: DebugElement = fixture.debugElement.query(By.css('datagrid-cell')); + let de: DebugElement[] = fixture.debugElement.queryAll(By.css('datagrid-cell')); fixture.detectChanges(); expect(de).toBeTruthy(); - let el: HTMLElement = de.nativeElement; + expect(de.length).toEqual(1); + let el: HTMLElement = de[0].nativeElement; + fixture.detectChanges(); expect(el).toBeTruthy(); expect(el.textContent).toEqual('library/nginx'); - expect(el.textContent).not.toEqual('library/busybox'); }); })); diff --git a/src/ui_ng/src/app/base/global-search/global-search.component.ts b/src/ui_ng/src/app/base/global-search/global-search.component.ts index ad3c810a3..e6dab1359 100644 --- a/src/ui_ng/src/app/base/global-search/global-search.component.ts +++ b/src/ui_ng/src/app/base/global-search/global-search.component.ts @@ -72,6 +72,10 @@ export class GlobalSearchComponent implements OnInit, OnDestroy { if (this.searchSub) { this.searchSub.unsubscribe(); } + + if (this.closeSub) { + this.closeSub.unsubscribe(); + } } //Handle the term inputting event diff --git a/src/ui_ng/src/app/project/create-project/create-project.component.html b/src/ui_ng/src/app/project/create-project/create-project.component.html index 7ebb4cc55..abf057aa8 100644 --- a/src/ui_ng/src/app/project/create-project/create-project.component.html +++ b/src/ui_ng/src/app/project/create-project/create-project.component.html @@ -1,40 +1,42 @@ - - - + + \ No newline at end of file diff --git a/src/ui_ng/src/app/project/create-project/create-project.component.ts b/src/ui_ng/src/app/project/create-project/create-project.component.ts index 1643e83d4..89635a905 100644 --- a/src/ui_ng/src/app/project/create-project/create-project.component.ts +++ b/src/ui_ng/src/app/project/create-project/create-project.component.ts @@ -11,7 +11,16 @@ // 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. -import { Component, EventEmitter, Output, ViewChild, AfterViewChecked, HostBinding } from '@angular/core'; +import { + Component, + EventEmitter, + Output, + ViewChild, + AfterViewChecked, + HostBinding, + OnInit, + OnDestroy +} from '@angular/core'; import { Response } from '@angular/http'; import { NgForm } from '@angular/forms'; @@ -23,13 +32,17 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com import { TranslateService } from '@ngx-translate/core'; +import { Subject } from 'rxjs/Subject'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; + @Component({ selector: 'create-project', templateUrl: 'create-project.component.html', - styleUrls: [ 'create-project.css' ] + styleUrls: ['create-project.css'] }) -export class CreateProjectComponent implements AfterViewChecked { - +export class CreateProjectComponent implements AfterViewChecked, OnInit, OnDestroy { + projectForm: NgForm; @ViewChild('projectForm') @@ -39,70 +52,112 @@ export class CreateProjectComponent implements AfterViewChecked { initVal: Project = new Project(); createProjectOpened: boolean; - + hasChanged: boolean; staticBackdrop: boolean = true; closable: boolean = false; + isNameValid: boolean = true; + nameTooltipText: string = 'PROJECT.NAME_TOOLTIP'; + checkOnGoing: boolean = false; + proNameChecker: Subject = new Subject(); + @Output() create = new EventEmitter(); @ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent; - constructor(private projectService: ProjectService, - private translateService: TranslateService, - private messageHandlerService: MessageHandlerService) {} + constructor(private projectService: ProjectService, + private translateService: TranslateService, + private messageHandlerService: MessageHandlerService) { } + + public get accessLevelDisplayText(): string { + return this.project.public ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE'; + } + + ngOnInit(): void { + this.proNameChecker + .debounceTime(500) + .distinctUntilChanged() + .subscribe((name: string) => { + let cont = this.currentForm.controls["create_project_name"]; + if (cont && this.hasChanged) { + this.isNameValid = cont.valid; + if (this.isNameValid) { + //Check exiting from backend + this.checkOnGoing = true; + this.projectService + .checkProjectExists(cont.value).toPromise() + .then(() => { + //Project existing + this.isNameValid = false; + this.nameTooltipText = 'PROJECT.NAME_ALREADY_EXISTS'; + this.checkOnGoing = false; + }) + .catch(error => { + this.checkOnGoing = false; + }); + } else { + this.nameTooltipText = 'PROJECT.NAME_TOOLTIP'; + } + } + }); + } + + ngOnDestroy(): void { + this.proNameChecker.unsubscribe(); + } onSubmit() { this.projectService - .createProject(this.project.name, this.project.public ? 1 : 0) - .subscribe( - status=>{ - this.create.emit(true); - this.messageHandlerService.showSuccess('PROJECT.CREATED_SUCCESS'); + .createProject(this.project.name, this.project.public ? 1 : 0) + .subscribe( + status => { + this.create.emit(true); + this.messageHandlerService.showSuccess('PROJECT.CREATED_SUCCESS'); + this.createProjectOpened = false; + }, + error => { + let errorMessage: string; + if (error instanceof Response) { + switch (error.status) { + case 409: + this.translateService.get('PROJECT.NAME_ALREADY_EXISTS').subscribe(res => errorMessage = res); + break; + case 400: + this.translateService.get('PROJECT.NAME_IS_ILLEGAL').subscribe(res => errorMessage = res); + break; + default: + this.translateService.get('PROJECT.UNKNOWN_ERROR').subscribe(res => errorMessage = res); + } + if (this.messageHandlerService.isAppLevel(error)) { + this.messageHandlerService.handleError(error); this.createProjectOpened = false; - }, - error=>{ - let errorMessage: string; - if (error instanceof Response) { - switch(error.status) { - case 409: - this.translateService.get('PROJECT.NAME_ALREADY_EXISTS').subscribe(res=>errorMessage = res); - break; - case 400: - this.translateService.get('PROJECT.NAME_IS_ILLEGAL').subscribe(res=>errorMessage = res); - break; - default: - this.translateService.get('PROJECT.UNKNOWN_ERROR').subscribe(res=>errorMessage = res); - } - if(this.messageHandlerService.isAppLevel(error)) { - this.messageHandlerService.handleError(error); - this.createProjectOpened = false; - } else { - this.inlineAlert.showInlineError(errorMessage); - } - } - }); + } else { + this.inlineAlert.showInlineError(errorMessage); + } + } + }); } onCancel() { - if(this.hasChanged) { - this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); + if (this.hasChanged) { + this.inlineAlert.showInlineConfirmation({ message: 'ALERT.FORM_CHANGE_CONFIRMATION' }); } else { this.createProjectOpened = false; this.projectForm.reset(); } - + } ngAfterViewChecked(): void { this.projectForm = this.currentForm; - if(this.projectForm) { - this.projectForm.valueChanges.subscribe(data=>{ - for(let i in data) { - let origin = this.initVal[i]; + if (this.projectForm) { + this.projectForm.valueChanges.subscribe(data => { + for (let i in data) { + let origin = this.initVal[i]; let current = data[i]; - if(current && current !== origin) { + if (current && current !== origin) { this.hasChanged = true; break; } else { @@ -125,5 +180,20 @@ export class CreateProjectComponent implements AfterViewChecked { this.inlineAlert.close(); this.projectForm.reset(); } + + public get isValid(): boolean { + return this.currentForm && + this.currentForm.valid && + this.isNameValid && + !this.checkOnGoing; + } + + //Handle the form validation + handleValidation(): void { + let cont = this.currentForm.controls["create_project_name"]; + if (cont) { + this.proNameChecker.next(cont.value); + } + } } diff --git a/src/ui_ng/src/app/project/create-project/create-project.css b/src/ui_ng/src/app/project/create-project/create-project.css index 0bcac82e9..9a9500f19 100644 --- a/src/ui_ng/src/app/project/create-project/create-project.css +++ b/src/ui_ng/src/app/project/create-project/create-project.css @@ -1,4 +1,13 @@ .form-group-label-override { font-size: 14px; font-weight: 400; +} + +.access-level-label { + font-size: 14px; + font-weight: 400; + margin-left: -4px; + margin-right: 12px; + top: -6px; + position: relative; } \ No newline at end of file diff --git a/src/ui_ng/src/app/project/member/add-member/add-member.component.html b/src/ui_ng/src/app/project/member/add-member/add-member.component.html index 9da8644b1..7bc567e28 100644 --- a/src/ui_ng/src/app/project/member/add-member/add-member.component.html +++ b/src/ui_ng/src/app/project/member/add-member/add-member.component.html @@ -1,41 +1,44 @@ - - - - + + \ No newline at end of file diff --git a/src/ui_ng/src/app/project/member/add-member/add-member.component.ts b/src/ui_ng/src/app/project/member/add-member/add-member.component.ts index 96126cbe1..335358f49 100644 --- a/src/ui_ng/src/app/project/member/add-member/add-member.component.ts +++ b/src/ui_ng/src/app/project/member/add-member/add-member.component.ts @@ -11,10 +11,19 @@ // 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. -import { Component, Input, EventEmitter, Output, ViewChild, AfterViewChecked } from '@angular/core'; +import { + Component, + Input, + EventEmitter, + Output, + ViewChild, + AfterViewChecked, + OnInit, + OnDestroy +} from '@angular/core'; import { Response } from '@angular/http'; import { NgForm } from '@angular/forms'; - + import { MemberService } from '../member.service'; import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service'; @@ -24,18 +33,21 @@ import { TranslateService } from '@ngx-translate/core'; import { Member } from '../member'; +import { Subject } from 'rxjs/Subject'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; + @Component({ selector: 'add-member', templateUrl: 'add-member.component.html', - styleUrls: [ 'add-member.component.css' ] + styleUrls: ['add-member.component.css'] }) -export class AddMemberComponent implements AfterViewChecked { +export class AddMemberComponent implements AfterViewChecked, OnInit, OnDestroy { member: Member = new Member(); - initVal: Member = new Member(); addMemberOpened: boolean; - + memberForm: NgForm; staticBackdrop: boolean = true; @@ -52,49 +64,87 @@ export class AddMemberComponent implements AfterViewChecked { @Input() projectId: number; @Output() added = new EventEmitter(); - constructor(private memberService: MemberService, - private messageHandlerService: MessageHandlerService, - private translateService: TranslateService) {} + isMemberNameValid: boolean = true; + memberTooltip: string = 'MEMBER.USERNAME_IS_REQUIRED'; + nameChecker: Subject = new Subject(); + checkOnGoing: boolean = false; + + constructor(private memberService: MemberService, + private messageHandlerService: MessageHandlerService, + private translateService: TranslateService) { } + + ngOnInit(): void { + this.nameChecker + .debounceTime(500) + .distinctUntilChanged() + .subscribe((name: string) => { + let cont = this.currentForm.controls['member_name']; + if (cont) { + this.isMemberNameValid = cont.valid; + if (cont.valid) { + this.checkOnGoing = true; + this.memberService + .listMembers(this.projectId, cont.value).toPromise() + .then((members: Member[]) => { + if (members.filter(m => { return m.username === cont.value }).length > 0) { + this.isMemberNameValid = false; + this.memberTooltip = 'MEMBER.USERNAME_ALREADY_EXISTS'; + } + this.checkOnGoing = false; + }) + .catch(error => { + this.checkOnGoing = false; + }); + } else { + this.memberTooltip = 'MEMBER.USERNAME_IS_REQUIRED'; + } + } + }); + } + + ngOnDestroy(): void { + this.nameChecker.unsubscribe(); + } onSubmit(): void { - if(!this.member.username || this.member.username.length === 0) { return; } + if (!this.member.username || this.member.username.length === 0) { return; } this.memberService - .addMember(this.projectId, this.member.username, +this.member.role_id) - .subscribe( - response=>{ - this.messageHandlerService.showSuccess('MEMBER.ADDED_SUCCESS'); - this.added.emit(true); - this.addMemberOpened = false; - }, - error=>{ - if (error instanceof Response) { - let errorMessageKey: string; - switch(error.status){ - case 404: - errorMessageKey = 'MEMBER.USERNAME_DOES_NOT_EXISTS'; - break; - case 409: - errorMessageKey = 'MEMBER.USERNAME_ALREADY_EXISTS'; - break; - default: - errorMessageKey = 'MEMBER.UNKNOWN_ERROR'; - } - if(this.messageHandlerService.isAppLevel(error)) { - this.messageHandlerService.handleError(error); - this.addMemberOpened = false; - } else { - this.translateService - .get(errorMessageKey) - .subscribe(errorMessage=>this.inlineAlert.showInlineError(errorMessage)); - } - } + .addMember(this.projectId, this.member.username, +this.member.role_id) + .subscribe( + response => { + this.messageHandlerService.showSuccess('MEMBER.ADDED_SUCCESS'); + this.added.emit(true); + this.addMemberOpened = false; + }, + error => { + if (error instanceof Response) { + let errorMessageKey: string; + switch (error.status) { + case 404: + errorMessageKey = 'MEMBER.USERNAME_DOES_NOT_EXISTS'; + break; + case 409: + errorMessageKey = 'MEMBER.USERNAME_ALREADY_EXISTS'; + break; + default: + errorMessageKey = 'MEMBER.UNKNOWN_ERROR'; } - ); + if (this.messageHandlerService.isAppLevel(error)) { + this.messageHandlerService.handleError(error); + this.addMemberOpened = false; + } else { + this.translateService + .get(errorMessageKey) + .subscribe(errorMessage => this.inlineAlert.showInlineError(errorMessage)); + } + } + } + ); } onCancel() { - if(this.hasChanged) { - this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); + if (this.hasChanged) { + this.inlineAlert.showInlineConfirmation({ message: 'ALERT.FORM_CHANGE_CONFIRMATION' }); } else { this.addMemberOpened = false; this.memberForm.reset(); @@ -102,19 +152,17 @@ export class AddMemberComponent implements AfterViewChecked { } ngAfterViewChecked(): void { - this.memberForm = this.currentForm; - if(this.memberForm) { - this.memberForm.valueChanges.subscribe(data=>{ - for(let i in data) { - let origin = this.initVal[i]; - let current = data[i]; - if(current && current !== origin) { - this.hasChanged = true; - break; - } else { - this.hasChanged = false; - this.inlineAlert.close(); - } + if (this.memberForm !== this.currentForm) { + this.memberForm = this.currentForm; + } + if (this.memberForm) { + this.memberForm.valueChanges.subscribe(data => { + let memberName = data['member_name']; + if (memberName && memberName !== '') { + this.hasChanged = true; + this.inlineAlert.close(); + } else { + this.hasChanged = false; } }); } @@ -132,6 +180,22 @@ export class AddMemberComponent implements AfterViewChecked { this.addMemberOpened = true; this.hasChanged = false; this.member.role_id = 1; + this.member.username = ''; + this.isMemberNameValid = true; + this.memberTooltip = 'MEMBER.USERNAME_IS_REQUIRED'; } + handleValidation(): void { + let cont = this.currentForm.controls['member_name']; + if (cont) { + this.nameChecker.next(cont.value); + } + } + + public get isValid(): boolean { + return this.currentForm && + this.currentForm.valid && + this.isMemberNameValid && + !this.checkOnGoing; + } } \ No newline at end of file diff --git a/src/ui_ng/src/i18n/lang/en-us-lang.json b/src/ui_ng/src/i18n/lang/en-us-lang.json index 07d76b2f5..1447a8b2c 100644 --- a/src/ui_ng/src/i18n/lang/en-us-lang.json +++ b/src/ui_ng/src/i18n/lang/en-us-lang.json @@ -39,7 +39,7 @@ "FULL_NAME": "Maximum length should be 20 characters.", "COMMENT": "Length of comment should be less than 20 characters.", "CURRENT_PWD": "Current password is required.", - "PASSWORD": "Password should be at least 8 characters with at least 1 uppercase, 1 lowercase and 1 number.", + "PASSWORD": "Password should be 8-20 characters long with at least 1 uppercase, 1 lowercase and 1 number.", "CONFIRM_PWD": "Passwords do not match.", "SIGN_IN_USERNAME": "Username is required.", "SIGN_IN_PWD": "Password is required.", @@ -76,7 +76,7 @@ "NEW_PWD": "New Password", "CONFIRM_PWD": "Confirm Password", "SAVE_SUCCESS": "User password changed successfully.", - "PASS_TIPS": "At least 8 characters with 1 uppercase, 1 lowercase and 1 number" + "PASS_TIPS": "8-20 characters long with 1 uppercase, 1 lowercase and 1 number" }, "ACCOUNT_SETTINGS": { "PROFILE": "User Profile", @@ -126,6 +126,7 @@ "PUBLIC_OR_PRIVATE": "Access Level", "REPO_COUNT": "Repositories Count", "CREATION_TIME": "Creation Time", + "ACCESS_LEVEL": "Access Level", "PUBLIC": "Public", "PRIVATE": "Private", "MAKE": "Make", @@ -135,6 +136,7 @@ "PUBLIC_PROJECTS": "Public Projects", "PROJECT": "Project", "NEW_PROJECT": "New Project", + "NAME_TOOLTIP": "Project name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.", "NAME_IS_REQUIRED": "Project name is required.", "NAME_MINIMUM_LENGTH": "Project name is too short, it should be greater than 2 characters.", "NAME_ALREADY_EXISTS": "Project name already exists.", diff --git a/src/ui_ng/src/i18n/lang/es-es-lang.json b/src/ui_ng/src/i18n/lang/es-es-lang.json index 087df658b..d924a54a2 100644 --- a/src/ui_ng/src/i18n/lang/es-es-lang.json +++ b/src/ui_ng/src/i18n/lang/es-es-lang.json @@ -39,7 +39,7 @@ "FULL_NAME": "La longitud máxima debería ser de 20 caracteres.", "COMMENT": "La longitud del comentario debería ser menor de 20 caracteres.", "CURRENT_PWD": "Es obligatorio introducir la contraseña actual.", - "PASSWORD": "La contraseña debería tener al menos 8 caracteres, con al menos 1 letra mayúscula, 1 letra minúscula y 1 número.", + "PASSWORD": "La contraseña debería tener de 8 a 20 caracteres, con al menos 1 letra mayúscula, 1 letra minúscula y 1 número.", "CONFIRM_PWD": "Las contraseñas no coinciden.", "SIGN_IN_USERNAME": "El nombre de usuario es obligatorio.", "SIGN_IN_PWD": "La contraseña es obligatoria.", @@ -76,7 +76,7 @@ "NEW_PWD": "Nueva contraseña", "CONFIRM_PWD": "Confirmar contraseña", "SAVE_SUCCESS": "Contraseña de usuario guardada satisfactoriamente.", - "PASS_TIPS": "Al menos 8 caracteres con 1 letra mayúscula, 1 minúscula y 1 número" + "PASS_TIPS": "8-20 caracteres con 1 letra mayúscula, 1 minúscula y 1 número" }, "ACCOUNT_SETTINGS": { "PROFILE": "Perfil de usuario", @@ -136,6 +136,7 @@ "PUBLIC_PROJECTS": "Proyectos Públicos", "PROJECT": "Proyecto", "NEW_PROJECT": "Nuevo proyecto", + "NAME_TOOLTIP": "Project name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.", "NAME_IS_REQUIRED": "El nombre del proyecto es obligatorio.", "NAME_MINIMUM_LENGTH": "El nombre del proyecto es demasiado corto, debe ser mayor de 2 caracteres.", "NAME_ALREADY_EXISTS": "Ya existe un proyecto con ese nombre.", diff --git a/src/ui_ng/src/i18n/lang/zh-cn-lang.json b/src/ui_ng/src/i18n/lang/zh-cn-lang.json index c25a4fe7c..4b57a3422 100644 --- a/src/ui_ng/src/i18n/lang/zh-cn-lang.json +++ b/src/ui_ng/src/i18n/lang/zh-cn-lang.json @@ -39,7 +39,7 @@ "FULL_NAME": "长度不能超过20。", "COMMENT": "长度不能超过20。", "CURRENT_PWD": "当前密码为必填项。", - "PASSWORD": "密码长度至少为8且需包含至少一个大写字符,一个小写字符和一个数字。", + "PASSWORD": "密码长度在8到20之间且需包含至少一个大写字符,一个小写字符和一个数字。", "CONFIRM_PWD": "密码输入不一致。", "SIGN_IN_USERNAME": "用户名为必填项。", "SIGN_IN_PWD": "密码为必填项。", @@ -76,7 +76,7 @@ "NEW_PWD": "新密码", "CONFIRM_PWD": "确认密码", "SAVE_SUCCESS": "成功更改用户密码。", - "PASS_TIPS": "至少8个字符且需包含至少一个大写字符、小写字符或者数字" + "PASS_TIPS": "8到20个字符且需包含至少一个大写字符、小写字符或者数字" }, "ACCOUNT_SETTINGS": { "PROFILE": "用户设置", @@ -126,6 +126,7 @@ "PUBLIC_OR_PRIVATE": "访问级别", "REPO_COUNT": "镜像仓库数", "CREATION_TIME": "创建时间", + "ACCESS_LEVEL": "访问级别", "PUBLIC": "公开", "PRIVATE": "私有", "MAKE": "设为", @@ -135,7 +136,8 @@ "PUBLIC_PROJECTS": "公开项目", "PROJECT": "项目", "NEW_PROJECT": "新建项目", - "NAME_IS_REQUIRED": "项目名称为必填项", + "NAME_TOOLTIP": "项目名称由小写字符、数字和._-组成且至少2个字符并以字符或者数字开头。", + "NAME_IS_REQUIRED": "项目名称为必填项。", "NAME_MINIMUM_LENGTH": "项目名称长度过短,至少多于2个字符。", "NAME_ALREADY_EXISTS": "项目名称已存在。", "NAME_IS_ILLEGAL": "项目名称非法。",