KEYCLOAK-9038: Add password page and refactor

This commit is contained in:
Stan Silvert 2018-12-06 15:22:51 -05:00
parent 3f13df81ab
commit 40071a95da
10 changed files with 287 additions and 65 deletions

View file

@ -17,6 +17,8 @@
import * as React from 'react';
import {Route, Link} from 'react-router-dom';
import * as moment from 'moment';
import {KeycloakService} from './keycloak-service/keycloak.service';
import {Logout} from './widgets/Logout';
@ -29,6 +31,8 @@ import {ExtensionPages} from './content/extensions/ExtensionPages';
declare function toggleReact():void;
declare function isWelcomePage(): boolean;
declare const locale: string;
export interface AppProps {};
export class App extends React.Component<AppProps> {
@ -48,6 +52,9 @@ export class App extends React.Component<AppProps> {
this.kcSvc.login();
}
// globally set up locale for date formatting
moment.locale(locale);
return (
<span>
<nav>

View file

@ -17,13 +17,13 @@
//import {KeycloakNotificationService} from '../notification/keycloak-notification.service';
import {KeycloakService} from '../keycloak-service/keycloak.service';
import Axios, {AxiosRequestConfig, AxiosResponse, AxiosPromise} from 'axios';
import Axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
//import {NotificationType} from 'patternfly-ng/notification';*/
type AxiosResolve = (AxiosResponse) => void;
type ConfigResolve = (AxiosRequestConfig) => void;
type ErrorReject = (Error) => void;
type AxiosResolve = (response: AxiosResponse) => void;
type ConfigResolve = (config: AxiosRequestConfig) => void;
type ErrorReject = (error: Error) => void;
/**
*
@ -88,7 +88,7 @@ export class AccountServiceClient {
console.log(error);
}
private makeConfig(endpoint: string, config?: AxiosRequestConfig): Promise<AxiosRequestConfig> {
private makeConfig(endpoint: string, config: AxiosRequestConfig = {}): Promise<AxiosRequestConfig> {
return new Promise( (resolve: ConfigResolve, reject: ErrorReject) => {
this.kcSvc.getToken()
.then( (token: string) => {

View file

@ -18,91 +18,150 @@ import * as React from 'react';
import {AxiosResponse} from 'axios';
import {AccountServiceClient} from '../../account-service/account.service';
import {Features} from '../../page/features';
import {Msg} from '../../widgets/Msg';
declare const features: Features;
interface AccountPageProps {
}
interface FormFields {
readonly username?: string;
readonly firstName?: string;
readonly lastName?: string;
readonly email?: string;
readonly emailVerified?: boolean;
}
interface AccountPageState {
readonly changed: boolean,
readonly username: string,
readonly firstName?: string,
readonly lastName?: string,
readonly email?: string,
readonly emailVerified?: boolean
readonly canSubmit: boolean;
readonly formFields: FormFields
}
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
*/
export class AccountPage extends React.Component<AccountPageProps, AccountPageState> {
readonly state: AccountPageState = {
changed: false,
username: '',
firstName: '',
lastName: '',
email: ''
private isRegistrationEmailAsUsername: boolean = features.isRegistrationEmailAsUsername;
private isEditUserNameAllowed: boolean = features.isEditUserNameAllowed;
state: AccountPageState = {
canSubmit: false,
formFields: {username: '',
firstName: '',
lastName: '',
email: ''}
};
constructor(props: AccountPageProps) {
super(props);
AccountServiceClient.Instance.doGet("/")
.then((response: AxiosResponse<AccountPageState>) => {
this.setState(response.data);
.then((response: AxiosResponse<FormFields>) => {
this.setState({formFields: response.data});
console.log({response});
});
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.makeTextInput = this.makeTextInput.bind(this);
}
private handleChange(event: React.ChangeEvent<HTMLInputElement>): void {
private handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const target: HTMLInputElement = event.target;
const value: string = target.value;
const name: string = target.name;
this.setState({
changed: true,
username: this.state.username,
[name]: value
} as AccountPageState);
canSubmit: this.requiredFieldsHaveData(name, value),
formFields: {...this.state.formFields, [name]:value}
});
}
private handleSubmit(event: React.FormEvent<HTMLFormElement>): void {
private handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
const reqData = {...this.state};
delete reqData.changed;
const reqData: FormFields = {...this.state.formFields};
AccountServiceClient.Instance.doPost("/", {data: reqData})
.then((response: AxiosResponse<AccountPageState>) => {
this.setState({changed: false});
.then((response: AxiosResponse<FormFields>) => {
this.setState({canSubmit: false});
alert('Data posted');
});
}
private makeTextInput(name: string,
disabled = false): React.ReactElement<any> {
return (
<label><Msg msgKey={name}/>:
<input disabled={disabled} type="text" name={name} value={this.state[name]} onChange={this.handleChange} />
</label>
);
private requiredFieldsHaveData(fieldName: string, newValue: string): boolean {
const fields: FormFields = {...this.state.formFields};
fields[fieldName] = newValue;
for (const field of Object.keys(fields)) {
if (!fields[field]) return false;
}
return true;
}
render() {
const fields: FormFields = this.state.formFields;
return (
<form onSubmit={this.handleSubmit}>
{this.makeTextInput('username', true)}
<br/>
{this.makeTextInput('firstName')}
<br/>
{this.makeTextInput('lastName')}
<br/>
{this.makeTextInput('email')}
<br/>
<button className="btn btn-primary btn-lg btn-sign"
disabled={!this.state.changed}
value="Submit"><Msg msgKey="doSave"/></button>
</form>
<span>
<div className="page-header">
<h1 id="pageTitle"><Msg msgKey="personalInfoHtmlTitle"/></h1>
</div>
<div className="col-sm-12 card-pf">
<div className="card-pf-body row">
<div className="col-sm-4 col-md-4">
<div className="card-pf-subtitle" id="personalSubTitle">
<Msg msgKey="personalSubTitle"/>
</div>
<div className="introMessage" id="personalSubMessage">
<p><Msg msgKey="personalSubMessage"/></p>
</div>
<div className="subtitle" id="requiredFieldMessage"><span className="required">*</span> <Msg msgKey="requiredFields"/></div>
</div>
<div className="col-sm-6 col-md-6">
<form onSubmit={this.handleSubmit} className="form-horizontal">
{ !this.isRegistrationEmailAsUsername &&
<div className="form-group ">
<label htmlFor="username" className="control-label"><Msg msgKey="username" /></label>{this.isEditUserNameAllowed && <span className="required">*</span>}
{this.isEditUserNameAllowed && <this.UsernameInput/>}
{!this.isEditUserNameAllowed && <this.RestrictedUsernameInput/>}
</div>
}
<div className="form-group ">
<label htmlFor="email" className="control-label"><Msg msgKey="email"/></label> <span className="required">*</span>
<input type="email" className="form-control" id="email" name="email" required autoFocus onChange={this.handleChange} value={fields.email}/>
</div>
<div className="form-group ">
<label htmlFor="firstName" className="control-label"><Msg msgKey="firstName"/></label> <span className="required">*</span>
<input className="form-control" id="firstName" required name="firstName" type="text" onChange={this.handleChange} value={fields.firstName}/>
</div>
<div className="form-group ">
<label htmlFor="lastName" className="control-label"><Msg msgKey="lastName"/></label> <span className="required">*</span>
<input className="form-control" id="lastName" required name="lastName" type="text" onChange={this.handleChange} value={fields.lastName}/>
</div>
<div className="form-group">
<div id="kc-form-buttons" className="submit">
<div className="">
<button disabled={!this.state.canSubmit}
type="submit" className="btn btn-primary btn-lg"
name="submitAction"><Msg msgKey="doSave"/></button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</span>
);
}
UsernameInput = () => (
<input type="text" className="form-control" required id="username" name="username" onChange={this.handleChange} value={this.state.formFields.username} />
);
RestrictedUsernameInput = () => (
<div className="non-edit" id="username">{this.state.formFields.username}</div>
);
};

View file

@ -15,21 +15,144 @@
*/
import * as React from 'react';
import * as moment from 'moment';
import {AxiosResponse} from 'axios';
import {AccountServiceClient} from '../../account-service/account.service';
import {Msg} from '../../widgets/Msg';
export interface PasswordPageProps {
}
export class PasswordPage extends React.Component<PasswordPageProps> {
interface FormFields {
readonly currentPassword?: string;
readonly newPassword?: string;
readonly confirmation?: string;
}
interface PasswordPageState {
readonly canSubmit: boolean,
readonly registered: boolean;
readonly lastUpdate: number;
readonly formFields: FormFields;
}
export class PasswordPage extends React.Component<PasswordPageProps, PasswordPageState> {
state: PasswordPageState = {
canSubmit: false,
registered: false,
lastUpdate: -1,
formFields: {currentPassword: '',
newPassword: '',
confirmation: ''}
}
constructor(props: PasswordPageProps) {
super(props);
AccountServiceClient.Instance.doGet("/credentials/password")
.then((response: AxiosResponse<PasswordPageState>) => {
this.setState({...response.data});
console.log({response});
});
}
private handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const target: HTMLInputElement = event.target;
const value: string = target.value;
const name: string = target.name;
this.setState({
canSubmit: this.requiredFieldsHaveData(name, value),
registered: this.state.registered,
lastUpdate: this.state.lastUpdate,
formFields: {...this.state.formFields, [name]: value}
});
}
private handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
const reqData: FormFields = {...this.state.formFields};
AccountServiceClient.Instance.doPost("/credentials/password", {data: reqData})
.then((response: AxiosResponse<FormFields>) => {
this.setState({canSubmit: false});
alert('Data posted');
});
}
private requiredFieldsHaveData(fieldName: string, newValue: string): boolean {
const fields: FormFields = {...this.state.formFields};
fields[fieldName] = newValue;
for (const field of Object.keys(fields)) {
if (!fields[field]) return false;
}
return true;
}
render() {
const displayNone = {display: 'none'};
return (
<div>
<h2>Hello Password Page</h2>
<div>
<div className="page-header">
<h1 id="pageTitle"><Msg msgKey="changePasswordHtmlTitle"/></h1>
</div>
<div className="col-sm-12 card-pf">
<div className="card-pf-body p-b" id="passwordLastUpdate">
<span className="i pficon pficon-info"></span>
<Msg msgKey="passwordLastUpdateMessage" /> <strong>{moment(this.state.lastUpdate).format('LLLL')}</strong>
</div>
</div>
<div className="col-sm-12 card-pf">
<div className="card-pf-body row">
<div className="col-sm-4 col-md-4">
<div className="card-pf-subtitle" id="updatePasswordSubTitle">
<Msg msgKey="updatePasswordTitle"/>
</div>
<div className="introMessage" id="updatePasswordSubMessage">
<strong><Msg msgKey="updatePasswordMessageTitle"/></strong>
<p><Msg msgKey="updatePasswordMessage"/></p>
</div>
<div className="subtitle"><span className="required">*</span> <Msg msgKey="requiredFields"/></div>
</div>
<div className="col-sm-6 col-md-6">
<form onSubmit={this.handleSubmit} className="form-horizontal">
<input readOnly value="this is not a login form" style={displayNone} type="text"/>
<input readOnly value="this is not a login form" style={displayNone} type="password"/>
<div className="form-group">
<label htmlFor="password" className="control-label"><Msg msgKey="currentPassword"/></label><span className="required">*</span>
<input onChange={this.handleChange} className="form-control" name="currentPassword" autoFocus autoComplete="off" type="password"/>
</div>
<div className="form-group">
<label htmlFor="password-new" className="control-label"><Msg msgKey="passwordNew"/></label><span className="required">*</span>
<input onChange={this.handleChange} className="form-control" id="newPassword" name="newPassword" autoComplete="off" type="password"/>
</div>
<div className="form-group">
<label htmlFor="password-confirm" className="control-label"><Msg msgKey="passwordConfirm"/></label><span className="required">*</span>
<input onChange={this.handleChange} className="form-control" id="confirmation" name="confirmation" autoComplete="off" type="password"/>
</div>
<div className="form-group">
<div id="kc-form-buttons" className="submit">
<div className="">
<button disabled={!this.state.canSubmit}
type="submit"
className="btn btn-primary btn-lg"
name="submitAction"><Msg msgKey="doSave"/></button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
);
}
};

View file

@ -49,7 +49,7 @@ export class KeycloakService {
* for details.
* @returns {Promise<T>}
*/
static init(configOptions?: string|{}, initOptions?: InitOptions): Promise<any> {
static init(configOptions?: string|{}, initOptions: InitOptions = {}): Promise<any> {
KeycloakService.keycloakAuth = Keycloak(configOptions);
return new Promise((resolve, reject) => {
@ -64,7 +64,7 @@ export class KeycloakService {
}
authenticated(): boolean {
return KeycloakService.keycloakAuth.authenticated;
return KeycloakService.keycloakAuth.authenticated ? KeycloakService.keycloakAuth.authenticated : false;
}
login(options?: KeycloakLoginOptions) {
@ -79,11 +79,11 @@ export class KeycloakService {
KeycloakService.keycloakAuth.accountManagement();
}
authServerUrl(): string {
authServerUrl(): string | undefined {
return KeycloakService.keycloakAuth.authServerUrl;
}
realm(): string {
realm(): string | undefined {
return KeycloakService.keycloakAuth.realm;
}

View file

@ -20,7 +20,7 @@ import {Link} from 'react-router-dom';
import {Msg} from './Msg';
import {KeycloakService} from '../keycloak-service/keycloak.service';
declare const baseUrl;
declare const baseUrl: string;
export interface LogoutProps {
}

View file

@ -262,6 +262,12 @@
"integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==",
"optional": true
},
"@types/history": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.2.tgz",
"integrity": "sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q==",
"dev": true
},
"@types/node": {
"version": "10.11.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.3.tgz",
@ -294,6 +300,27 @@
"@types/react": "*"
}
},
"@types/react-router": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-4.4.1.tgz",
"integrity": "sha512-CtQfdcXyMye3vflnQQ2sHU832iDJRoAr4P+7f964KlLYupXU1I5crP1+d/WnCMo6mmtjBjqQvxrtbAbodqerMA==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-4.3.1.tgz",
"integrity": "sha512-GbztJAScOmQ/7RsQfO4cd55RuH1W4g6V1gDW3j4riLlt+8yxYLqqsiMzmyuXBLzdFmDtX/uU2Bpcm0cmudv44A==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"axios": {
"version": "0.18.0",
"resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz",

View file

@ -3,8 +3,8 @@
"version": "1.0.0",
"description": "keycloak-preview account management written in React",
"scripts": {
"build": "tsc --jsx react -p ./",
"build:watch": "tsc --jsx react -p ./ -w"
"build": "tsc --noImplicitAny --strictNullChecks --jsx react -p ./",
"build:watch": "tsc --noImplicitAny --strictNullChecks --jsx react -p ./ -w"
},
"keywords": [],
"author": "Stan Silvert",
@ -12,6 +12,7 @@
"dependencies": {
"axios": "^0.18.0",
"bootstrap": "^4.1.0",
"moment": "^2.22.2",
"patternfly": "^3.23.2",
"react": "^16.5.2",
"react-dom": "^16.5.2",
@ -22,6 +23,7 @@
"devDependencies": {
"@types/react": "^16.4.14",
"@types/react-dom": "^16.0.8",
"@types/react-router-dom": "^4.3.1",
"typescript": "^3.1.1"
},
"repository": {}

View file

@ -18,6 +18,8 @@
'react-dom': 'npm:react-dom/umd/react-dom.development.js',
'react-router-dom': 'npm:react-router-dom/umd/react-router-dom.js',
'moment': 'npm:moment/min/moment-with-locales.min.js',
'axios': 'npm:axios/dist/axios.min.js',
},

View file

@ -7,7 +7,9 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": false,
"noImplicitAny": true,
"strictNullChecks": true,
"jsx": "react",
"suppressImplicitAnyIndexErrors": true
}
}