Improve login page for OIDC (#15214)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2021-07-02 09:42:04 +08:00 committed by GitHub
parent 29ccdff766
commit bb57264f11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 137 additions and 98 deletions

View File

@ -2,55 +2,80 @@
<global-message [isAppLevel]="true"></global-message> <global-message [isAppLevel]="true"></global-message>
<navigator (showDialogModalAction)="openModal($event)"></navigator> <navigator (showDialogModalAction)="openModal($event)"></navigator>
<search-result></search-result> <search-result></search-result>
<div class="login-wrapper" [ngStyle]="{'background-image': customLoginBgImg? 'url(/images/' + customLoginBgImg + ')': ''}"> <div class="login-wrapper"
[ngStyle]="{'background-image': customLoginBgImg? 'url(/images/' + customLoginBgImg + ')': ''}">
<form #signInForm="ngForm" class="login"> <form #signInForm="ngForm" class="login">
<label class="title"> {{customAppTitle? customAppTitle:(appTitle | translate)}} <span *ngIf="isOidcLoginMode && steps===2" (click)="steps=1" class="back-icon">
<clr-icon shape="angle left"></clr-icon>
<div class="margin-left-3">{{'SEARCH.BACK' | translate }}</div>
</span>
<label class="title"> {{customAppTitle ? customAppTitle : (appTitle | translate)}}
</label> </label>
<a href="/c/oidc/login" class="login-oidc" *ngIf="isOidcLoginMode">
<button type="button" id="log_oidc" class="btn btn-primary btn-block">
<span>{{'BUTTON.LOG_IN_OIDC' | translate }}</span>
</button>
</a>
<div class="login-group"> <div class="login-group">
<clr-input-container> <ng-container *ngIf="isOidcLoginMode && steps ===1">
<input clrInput class="username" type="text" required [(ngModel)]="signInCredential.principal" name="login_username" id="login_username" <a href="/c/oidc/login">
placeholder='{{"PLACEHOLDER.SIGN_IN_NAME" | translate}}' #userNameInput='ngModel'> <button type="button" id="log_oidc" class="btn btn-primary btn-block">
<clr-control-error>{{ 'TOOLTIP.SIGN_IN_USERNAME' | translate }}</clr-control-error> <span>{{'BUTTON.LOG_IN_OIDC' | translate }}</span>
</clr-input-container> </button>
<div class="clr-form-control"> </a>
<div class="clr-control-container" [class.clr-error] ="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)"> <div class="divider-container mt-1 mb-1">
<div class="clr-input-wrapper"> <hr class="divider" size="1" noshade/>
<input class="clr-input pwd-input" [type]="showPwd?'text':'password'" required [(ngModel)]="signInCredential.password" name="login_password" id="login_password" <h4 class="m-0"><span>{{'SIGN_IN.OR' | translate }}</span></h4>
placeholder='{{"PLACEHOLDER.SIGN_IN_PWD" | translate}}' #passwordInput="ngModel"> <hr class="divider" size="1" noshade/>
<clr-icon *ngIf="!showPwd" shape="eye" class="pw-eye" (click)="showPwd =!showPwd"></clr-icon>
<clr-icon *ngIf="showPwd" shape="eye-hide" class="pw-eye" (click)="showPwd =!showPwd"></clr-icon>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
{{ 'TOOLTIP.SIGN_IN_PWD' | translate }}
</clr-control-error>
</div> </div>
<a href="javascript:void(0)" (click)="steps=2">
<button type="button" id="login-db" class="btn btn-primary btn-block mt-0">
<span>{{'SIGN_IN.VIA_LOCAL_DB' | translate }}</span>
</button>
</a>
</ng-container>
<ng-container *ngIf="(!isOidcLoginMode && steps === 1) || (isOidcLoginMode && steps ===2)">
<clr-input-container class="mt-3">
<input clrInput class="username" type="text" required [(ngModel)]="signInCredential.principal"
name="login_username" id="login_username"
placeholder='{{"PLACEHOLDER.SIGN_IN_NAME" | translate}}' #userNameInput='ngModel'>
<clr-control-error>{{ 'TOOLTIP.SIGN_IN_USERNAME' | translate }}</clr-control-error>
</clr-input-container>
<div class="clr-form-control">
<div class="clr-control-container"
[class.clr-error]="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
<div class="clr-input-wrapper">
<input class="clr-input pwd-input" [type]="showPwd?'text':'password'" required
[(ngModel)]="signInCredential.password" name="login_password" id="login_password"
placeholder='{{"PLACEHOLDER.SIGN_IN_PWD" | translate}}' #passwordInput="ngModel">
<clr-icon *ngIf="!showPwd" shape="eye" class="pw-eye"
(click)="showPwd =!showPwd"></clr-icon>
<clr-icon *ngIf="showPwd" shape="eye-hide" class="pw-eye"
(click)="showPwd =!showPwd"></clr-icon>
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error
*ngIf="passwordInput.invalid && (passwordInput.dirty || passwordInput.touched)">
{{ 'TOOLTIP.SIGN_IN_PWD' | translate }}
</clr-control-error>
</div>
</div>
<div class="display-flex">
<clr-checkbox-wrapper>
<input clrCheckbox type="checkbox" id="rememberme" #rememberMeBox
(click)="clickRememberMe($event)" [checked]="rememberMe">
<label class="reset-label" for="rememberme">{{ 'SIGN_IN.REMEMBER' | translate }}</label>
</clr-checkbox-wrapper>
</div>
<div *ngIf="isError" class="error active">
<span *ngIf="isCoreServiceAvailable">{{ 'SIGN_IN.INVALID_MSG' | translate }}</span>
<span *ngIf="!isCoreServiceAvailable">{{ 'SIGN_IN.CORE_SERVICE_NOT_AVAILABLE' | translate }}</span>
</div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary mt-2" (click)="signIn()"
id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()"
*ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</ng-container>
<div class="more-info" [ngClass]="{'pt-1': (!isOidcLoginMode && steps === 1) || (isOidcLoginMode && steps ===2)}">
<a href="https://github.com/goharbor/harbor" target="_blank">{{ 'BUTTON.MORE_INFO' | translate }}</a>
</div> </div>
<div class="display-flex">
<clr-checkbox-wrapper>
<input clrCheckbox type="checkbox" id="rememberme" #rememberMeBox (click)="clickRememberMe($event)" [checked]="rememberMe">
<label class="reset-label" for="rememberme">{{ 'SIGN_IN.REMEMBER' | translate }}</label>
</clr-checkbox-wrapper>
</div>
<div [class.visibility-hidden]="!isError" class="error active">
<span *ngIf="isCoreServiceAvailable">{{ 'SIGN_IN.INVALID_MSG' | translate }}</span>
<span *ngIf="!isCoreServiceAvailable">{{ 'SIGN_IN.CORE_SERVICE_NOT_AVAILABLE' | translate }}</span>
</div>
<button [disabled]="isOnGoing || !isValid" type="submit" class="btn btn-primary" (click)="signIn()" id="log_in">{{ 'BUTTON.LOG_IN' | translate }}</button>
<a href="javascript:void(0)" class="signup" (click)="signUp()" *ngIf="selfSignUp">{{ 'BUTTON.SIGN_UP_LINK' | translate }}</a>
</div>
<div>
<a href="https://github.com/goharbor/harbor" target="_blank" class="more-info-link">{{ 'BUTTON.MORE_INFO' | translate }}</a>
</div> </div>
</form> </form>
<div *ngIf="appConfig.show_popular_repo" id="pop_repo" class="popular-repo-wrapper">
<top-repo class="repo-container"></top-repo>
</div>
</div> </div>
</clr-main-container> </clr-main-container>
<sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up> <sign-up #signupDialog (userCreation)="handleUserCreation($event)"></sign-up>

View File

@ -14,31 +14,6 @@
top: -5px; top: -5px;
} }
.popular-repo-wrapper {
background-color: white;
min-height: 100vh;
display: flex;
flex-grow: 1;
margin-top: 24px;
justify-content: center;
flex-direction: column;
height: auto;
position: relative;
border-left: 1px solid #eee;
}
.repo-container {
width: 100%;
margin-top: -210px;
}
.more-info-link {
position: relative;
top: 80px;
left: 294px;
padding-right: 36px;
}
.tm-font { .tm-font {
font-size: 14px !important; font-size: 14px !important;
position: relative; position: relative;
@ -59,10 +34,6 @@
background:transparent; background:transparent;
} }
} }
.login-oidc{
margin-top: 50px;
}
.reset-label { .reset-label {
font-size:inherit; font-size:inherit;
color:inherit; color:inherit;
@ -82,3 +53,41 @@
.pwd-input { .pwd-input {
padding-right: 26px; padding-right: 26px;
} }
.divider-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.divider {
margin: 0;
width: 8.5rem;
}
.login {
padding-bottom: 180px;
}
.back-icon {
position: absolute;
top: 1rem;
display: flex;
align-items: center;
color: #007cbb;
font-size: 16px;
cursor: pointer;
}
.margin-left-3 {
margin-left: 3px;
}
.title {
position: absolute;
top: 10rem
}
.login-group {
width: 70%;
position: absolute;
top: 10rem
}
.more-info {
text-align: right;
padding-top: 2rem;
}

View File

@ -28,6 +28,7 @@ import { AboutDialogComponent } from "../../shared/components/about-dialog/about
import { CommonRoutes, CONFIG_AUTH_MODE } from "../../shared/entities/shared.const"; import { CommonRoutes, CONFIG_AUTH_MODE } from "../../shared/entities/shared.const";
import { SignInCredential } from "./sign-in-credential"; import { SignInCredential } from "./sign-in-credential";
import { UserPermissionService } from "../../shared/services"; import { UserPermissionService } from "../../shared/services";
import {finalize} from "rxjs/operators";
// Define status flags for signing in states // Define status flags for signing in states
export const signInStatusNormal = 0; export const signInStatusNormal = 0;
@ -45,7 +46,6 @@ const expireDays = 10;
export class SignInComponent implements AfterViewChecked, OnInit { export class SignInComponent implements AfterViewChecked, OnInit {
showPwd: boolean = false; showPwd: boolean = false;
redirectUrl: string = ""; redirectUrl: string = "";
appConfig: AppConfig = new AppConfig();
// Remeber me indicator // Remeber me indicator
rememberMe: boolean = false; rememberMe: boolean = false;
rememberedName: string = ""; rememberedName: string = "";
@ -68,6 +68,8 @@ export class SignInComponent implements AfterViewChecked, OnInit {
password: "" password: ""
}; };
isCoreServiceAvailable: boolean = true; isCoreServiceAvailable: boolean = true;
steps: number = 1;
hasLoadedAppConfig: boolean = false;
constructor( constructor(
private router: Router, private router: Router,
private session: SessionService, private session: SessionService,
@ -88,15 +90,6 @@ export class SignInComponent implements AfterViewChecked, OnInit {
this.customAppTitle = customSkinObj.loginTitle; this.customAppTitle = customSkinObj.loginTitle;
} }
} }
// Before login: Make sure the updated configuration can be loaded
this.appConfigService.load()
.subscribe(updatedConfig => this.appConfig = updatedConfig
, error => {
// Catch the error
console.error("Failed to load bootstrap options with error: ", error);
});
this.route.queryParams this.route.queryParams
.subscribe(params => { .subscribe(params => {
this.redirectUrl = params["redirect_url"] || ""; this.redirectUrl = params["redirect_url"] || "";
@ -117,7 +110,7 @@ export class SignInComponent implements AfterViewChecked, OnInit {
// App title // App title
public get appTitle(): string { public get appTitle(): string {
if (this.appConfig && this.appConfig.with_admiral) { if (this.appConfigService.getConfig() && this.appConfigService.getConfig().with_admiral) {
return "APP_TITLE.VIC"; return "APP_TITLE.VIC";
} }
@ -140,11 +133,11 @@ export class SignInComponent implements AfterViewChecked, OnInit {
// Whether show the 'sign up' link // Whether show the 'sign up' link
public get selfSignUp(): boolean { public get selfSignUp(): boolean {
return this.appConfig.auth_mode === CONFIG_AUTH_MODE.DB_AUTH return this.appConfigService.getConfig() && this.appConfigService.getConfig().auth_mode === CONFIG_AUTH_MODE.DB_AUTH
&& this.appConfig.self_registration; && this.appConfigService.getConfig().self_registration;
} }
public get isOidcLoginMode(): boolean { public get isOidcLoginMode(): boolean {
return this.appConfig.auth_mode === CONFIG_AUTH_MODE.OIDC_AUTH; return this.appConfigService.getConfig() && this.appConfigService.getConfig().auth_mode === CONFIG_AUTH_MODE.OIDC_AUTH;
} }
clickRememberMe($event: any): void { clickRememberMe($event: any): void {
if ($event && $event.target) { if ($event && $event.target) {
@ -261,11 +254,7 @@ export class SignInComponent implements AfterViewChecked, OnInit {
// after login successfully: Make sure the updated configuration can be loaded // after login successfully: Make sure the updated configuration can be loaded
this.appConfigService.load() this.appConfigService.load()
.subscribe(updatedConfig => this.appConfig = updatedConfig .subscribe();
, error => {
// Catch the error
console.error("Failed to load bootstrap options with error: ", error);
});
}, error => { }, error => {
// 403 oidc login no body; // 403 oidc login no body;
if (this.isOidcLoginMode && error && error.status === 403) { if (this.isOidcLoginMode && error && error.status === 403) {

View File

@ -14,6 +14,8 @@
"INVALID_MSG": "Falscher Nutzername oder Passwort.", "INVALID_MSG": "Falscher Nutzername oder Passwort.",
"FORGOT_PWD": "Passwort vergessen", "FORGOT_PWD": "Passwort vergessen",
"HEADER_LINK": "Einloggen", "HEADER_LINK": "Einloggen",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB",
"CORE_SERVICE_NOT_AVAILABLE": "Core Service ist nicht verfügbar" "CORE_SERVICE_NOT_AVAILABLE": "Core Service ist nicht verfügbar"
}, },
"SIGN_UP": { "SIGN_UP": {

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Invalid user name or password.", "INVALID_MSG": "Invalid user name or password.",
"FORGOT_PWD": "Forgot password", "FORGOT_PWD": "Forgot password",
"HEADER_LINK": "Sign In", "HEADER_LINK": "Sign In",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available." "CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "Sign Up" "TITLE": "Sign Up"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Nombre o contraseña no válidos.", "INVALID_MSG": "Nombre o contraseña no válidos.",
"FORGOT_PWD": "Olvidé mi contraseña", "FORGOT_PWD": "Olvidé mi contraseña",
"HEADER_LINK": "Identificarse", "HEADER_LINK": "Identificarse",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available." "CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "Registrarse" "TITLE": "Registrarse"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Nom d'utilisateur ou mot de passe invalide.", "INVALID_MSG": "Nom d'utilisateur ou mot de passe invalide.",
"FORGOT_PWD": "Mot de passe oublié", "FORGOT_PWD": "Mot de passe oublié",
"HEADER_LINK": "S'identifier", "HEADER_LINK": "S'identifier",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available." "CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "S'inscrire" "TITLE": "S'inscrire"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Usuário ou senha inválidos", "INVALID_MSG": "Usuário ou senha inválidos",
"FORGOT_PWD": "Esqueci a senha", "FORGOT_PWD": "Esqueci a senha",
"HEADER_LINK": "Logar-se", "HEADER_LINK": "Logar-se",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available." "CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "Registrar-se" "TITLE": "Registrar-se"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "Geçersiz kullanıcı adı veya şifre.", "INVALID_MSG": "Geçersiz kullanıcı adı veya şifre.",
"FORGOT_PWD": "Parolamı Unuttum", "FORGOT_PWD": "Parolamı Unuttum",
"HEADER_LINK": "Oturum aç", "HEADER_LINK": "Oturum aç",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available." "CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "Kayıt ol" "TITLE": "Kayıt ol"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "用户名或者密码不正确。", "INVALID_MSG": "用户名或者密码不正确。",
"FORGOT_PWD": "忘记密码", "FORGOT_PWD": "忘记密码",
"HEADER_LINK": "登录", "HEADER_LINK": "登录",
"CORE_SERVICE_NOT_AVAILABLE": "核心服务不可用。" "CORE_SERVICE_NOT_AVAILABLE": "核心服务不可用。",
"OR": "或",
"VIA_LOCAL_DB": "通过本地数据库登录"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "注册" "TITLE": "注册"

View File

@ -14,7 +14,9 @@
"INVALID_MSG": "用戶名或者密碼不正確。", "INVALID_MSG": "用戶名或者密碼不正確。",
"FORGOT_PWD": "忘記密碼", "FORGOT_PWD": "忘記密碼",
"HEADER_LINK": "登錄", "HEADER_LINK": "登錄",
"CORE_SERVICE_NOT_AVAILABLE": "Core service is not available." "CORE_SERVICE_NOT_AVAILABLE": "Core service is not available.",
"OR": "OR",
"VIA_LOCAL_DB": "LOGIN VIA LOCAL DB"
}, },
"SIGN_UP": { "SIGN_UP": {
"TITLE": "註冊" "TITLE": "註冊"