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

View file

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

View file

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

View file

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

View file

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

View file

@ -262,6 +262,12 @@
"integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==", "integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q==",
"optional": true "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": { "@types/node": {
"version": "10.11.3", "version": "10.11.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.3.tgz",
@ -294,6 +300,27 @@
"@types/react": "*" "@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": { "axios": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz",

View file

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

View file

@ -18,6 +18,8 @@
'react-dom': 'npm:react-dom/umd/react-dom.development.js', 'react-dom': 'npm:react-dom/umd/react-dom.development.js',
'react-router-dom': 'npm:react-router-dom/umd/react-router-dom.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', 'axios': 'npm:axios/dist/axios.min.js',
}, },

View file

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