KEYCLOAK-9038: Add password page and refactor
This commit is contained in:
parent
3f13df81ab
commit
40071a95da
10 changed files with 287 additions and 65 deletions
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [ "es2015", "dom" ],
|
||||
"noImplicitAny": false,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"jsx": "react",
|
||||
"suppressImplicitAnyIndexErrors": true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue