keycloak-uncached
Changes
themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts 10(+5 -5)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/AccountPage.tsx 155(+107 -48)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/PasswordPage.tsx 131(+127 -4)
Details
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts
index adf2970..1d34576 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/account-service/account.service.ts
@@ -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) => {
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/App.tsx b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/App.tsx
index a461d53..8f103fb 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/App.tsx
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/App.tsx
@@ -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>
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/AccountPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/AccountPage.tsx
index 6a75165..fb8f488 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/AccountPage.tsx
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/AccountPage.tsx
@@ -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>
+    );
+    
 };
\ No newline at end of file
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/PasswordPage.tsx b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/PasswordPage.tsx
index 59b5f8b..5db72c4 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/PasswordPage.tsx
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/PasswordPage.tsx
@@ -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>
         );
     }
 };
\ No newline at end of file
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts
index fbf3f14..afdded7 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts
@@ -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;
     }
 
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/widgets/Logout.tsx b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/widgets/Logout.tsx
index b041dcb..4ccf718 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/widgets/Logout.tsx
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/widgets/Logout.tsx
@@ -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 {
 }
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/package.json b/themes/src/main/resources/theme/keycloak-preview/account/resources/package.json
index 9d79dec..cd94720 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/package.json
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/package.json
@@ -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": {}
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/package-lock.json b/themes/src/main/resources/theme/keycloak-preview/account/resources/package-lock.json
index d90b324..f7cc76d 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/package-lock.json
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/package-lock.json
@@ -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",
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js b/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
index 7f1f5d6..6898f1f 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
@@ -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',
     },
     
                diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/tsconfig.json b/themes/src/main/resources/theme/keycloak-preview/account/resources/tsconfig.json
index 99f35dc..f835db0 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/tsconfig.json
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/tsconfig.json
@@ -7,7 +7,9 @@
     "emitDecoratorMetadata": true,
     "experimentalDecorators": true,
     "lib": [ "es2015", "dom" ],
-    "noImplicitAny": false,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "jsx": "react",
     "suppressImplicitAnyIndexErrors": true
   }
 }