Updates for Keycloak v16: updated D4Science themes and implemented D4Science.v2 account extra page

This commit is contained in:
Vincenzo Cestone 2022-02-16 10:09:31 +01:00
parent 5f43c1f3e5
commit feb1fe25b0
33 changed files with 1404 additions and 976 deletions

View File

@ -1,6 +1,17 @@
# Changelog for "keycloak-d4science-themes"
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Changelog for "keycloak-d4science-theme"
## Master branch - 2022-02-15
- Updated d4science themes for Keycloak v16
- Reimplemented d4science.v2 account ui for Keycloak v16
## Master branch
- First release
[#19518] Implemented the Keycloak "d4science" login parametric template.
Each new gateway that does not implement its own specific login template should use by default this base "d4science" template. However, it is possible to set some parameters in `<keycloak_home>/themes/<gatewayname>/login/themes.properties` file to do some customizations such as logo, background, colors, and some links (see for example parameters of dev4.d4science.org and next.d4science.org themes).
## [v2.0.0-SNAPSHOT]

View File

@ -1,52 +1,71 @@
# Keycloak D4Science Theme
# Keycloak D4Science Themes
**Keycloak D4Science Theme** repository collects the implementations of base D4Science theme and a set of specific per gateway themes implementations.
**Keycloak D4Science Themes** repository collects the implementations of base D4Science theme and a set of specific per gateway themes implementations.
Each Keycloak theme is made of a set of [Freemarker](https://freemarker.apache.org/) templates.
Each Keycloak theme is made of a set of [Freemarker](https://freemarker.apache.org/) templates. Themes of Keycloak account section is implemented in [React](https://reactjs.org/).
## Structure of the project
The themes sources are contained in `src/main/resources` folder.
The themes are in `src/main/resources/themes` subfolder.
The `util` subfolder contains some utility test scripts to create Keycloak clients that uses a specific theme.
## Built With
* [Maven](https://maven.apache.org/) - Dependency Management
[Node.js](https://nodejs.org/). See [Keycloak src project](https://github.com/keycloak/keycloak/tree/main/themes/src/main/resources/theme/keycloak.v2/account/src)
## Documentation
To build the theme JAR it is sufficient to type
mvn clean package
For details see [Theme section](https://www.keycloak.org/docs/latest/server_development/#_themes) of Keycloak developer docs.
As specified in [Deploying Themes](https://www.keycloak.org/docs/latest/server_development/#deploying-themes) section of the documentation, *themes can be deployed to Keycloak by copying the theme directory to themes or it can be deployed as an archive.*
During the development phase, themes caching in Keycloak configuration file has to be disabled. To do this edit `standalone.xml`. For `theme` set `staticMaxAge` to `-1` and both `cacheTemplates` and `cacheThemes` to `false`.
In the development phase, we suggest to disable themes caching in Keycloak configuration file. To do this edit `standalone.xml`. For `theme` set `staticMaxAge` to `-1` and both `cacheTemplates` and `cacheThemes` to `false`.
To test theme in a Keycloak instance you can clone this repo somewhere and create a symbolic link for each theme you want to deploy:
mkdir /opt/git
cd /opt/git
git clone https://code-repo.d4science.org/gCubeSystem/d4science-keycloak-themes.git
cd /opt/keycloak/themes
ln -s /opt/git/d4science-keycloak-themes/src/themes/d4science/
ln -s /opt/git/d4science-keycloak-themes/src/themes/d4science.v2/
ln -s /opt/git/d4science-keycloak-themes/src/themes/dev4.d4science.org/
ln -s /opt/git/d4science-keycloak-themes/src/themes/next.d4science.org/
...
Note: `d4science` and `d4science.v2` are the base D4Science themes, that are **required** by all `\*.d4science.org` per gateway specific themes.
If you are already logged-in in Keycloak administration console you have to logout and then re-login. At that you can set one of the new themes per Keycloak Realm or for a specific Keycloak Client in the realm.
## Change log
See [CHANGELOG.md](CHANGELOG.md).
See [CHANGELOG.md](CHANGELOG.md) file for details
See [Releases](https://code-repo.d4science.org/gCubeSystem/d4science-keycloak-themes/releases).
## Authors
* **Vincenzo Cestone** ([Nubisware S.r.l.](http://www.nubisware.com)) for themes preparing/testing.
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com)) for archive building/maven automation.
## How to Cite this Software
[Intentionally left blank]
* **Vincenzo Cestone** ([Nubisware S.r.l.](http://www.nubisware.com))
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com))
## License
This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details.
## About the gCube Framework
This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an
open-source software toolkit used for building and operating Hybrid Data
Infrastructures enabling the dynamic deployment of Virtual Research Environments
by favouring the realisation of reuse oriented policies.
The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md)
The projects leading to this software have received funding from a series of European Union programmes including:
## Acknowledgments
[Intentionally left blank]
- the Sixth Framework Programme for Research and Technological Development
- DILIGENT (grant no. 004260);
- the Seventh Framework Programme for research, technological development and demonstration
- D4Science (grant no. 212488), D4Science-II (grant no.239019), ENVRI (grant no. 283465), EUBrazilOpenBio (grant no. 288754), iMarine(grant no. 283644);
- the H2020 research and innovation programme
- BlueBRIDGE (grant no. 675680), EGIEngage (grant no. 654142), ENVRIplus (grant no. 654182), Parthenos (grant no. 654119), SoBigData (grant no. 654024),DESIRA (grant no. 818194), ARIADNEplus (grant no. 823914), RISIS2 (grant no. 824091), PerformFish (grant no. 727610), AGINFRAplus (grant no. 731001);

View File

@ -45,6 +45,12 @@
"login"
]
},
{
"name": "d4science.v2",
"types": [
"account"
]
},
{
"name": "dante.d4science.org",
"types": [

View File

@ -0,0 +1,24 @@
accountManagementWelcomeMessage=Welcome to Keycloak Extended Account Console
personalBasicInfoHtmlTitle=Basic information
accountExtraInfoHtmlTitleHome=Profile settings
accountExtraSubMessageHome=Handle extra profile settings
accountExtraInfoHtmlTitle=Profile settings
accountExtraSubMessage=Handle profile: add/update your avatar, delete your account
avatarLabel=Avatar
uploadLabel=Select image file
dragdropInfo=Drag and drop an image file or upload one
browseButton=Browse
clearButton=Clear
avatarInfo=A 100x100px size is suggested. Images exceding 250x250px will be resized to 250px width or height mantaining their ratio. The maximum file size permitted is 1MB.
avatarUpdatedMessage=Avatar successfully updated
error-noAvatarFound=No avatar found on server
deleteAccount=Delete Account
deleteAccountInfoMessage=Deleting your account will disable your profile and remove your name and photo you''ve shared on D4Science gateway(s). Some information may still be visible to others, such as your name in the posts and private messages you sent.<br/>All files and folders you created of your workspace will be removed.
deleteAccountDialogHeader=Confirm account delete
deleteAccountWarningMessage=Clicking on the "Confirm" button is an undoable operation, your account will be removed and you'll be automatically logged out from all your sessions.
deleteAccountConfirmMessage=<br/>Do you really want to remove your account? <strong>NOTE: This action is irreversible!</strong>
doDeleteConfirm=Confirm
accountDeletedMessage=Your account has been deleted

View File

@ -0,0 +1,24 @@
accountManagementWelcomeMessage=Benvenuto nella gestione estesa degli account di Keycloak
personalBasicInfoHtmlTitle=Informazioni base
accountExtraInfoHtmlTitleHome=Impostazioni profilo
accountExtraSubMessageHome=Gestisce ulteriori impostazioni associate al profilo
accountExtraInfoHtmlTitle=Impostazioni del profilo
accountExtraSubMessage=Gestisce il profilo: aggiunta/modifica dell''avatar, cancellazione dell''account
avatarLabel=Avatar
uploadLabel=Seleziona un''immagine
dragdropInfo=Trascina qui un file immagine o selezionalo
browseButton=Seleziona
clearButton=Cancella
avatarInfo=Si consiglia una dimensione di 100x100px. Le immagini che eccedono 250x250px saranno ridimensionate a 250px di larghezza o altezza mantenendo il loro rapporto. La massima dimensione consentita del file \u00e8 di 1MB.
avatarUpdatedMessage=Avatar aggiornato con successo
error-noAvatarFound=Avatar non trovato sul server
deleteAccount=Delete Account
deleteAccountInfoMessage=La cancellazione del proprio account disabiliter\u00e0 il profilo e rimuover\u00e0 il nome e le foto condivise sul/sui gateway D4Science. Alcune informazioni potrebbero risultare ancora visibili agli altri utenti, come il nome nei post e nei messaggi privati inviati.<br/>Tutti i file e le cartelle create nel workspace personale saranno rimosse.
deleteAccountDialogHeader=Conferma cancellazione account
deleteAccountWarningMessage=Cliccando sul bottone "Conferma" si avvier\u00e0 un''operazione non annullabile, l''account personale sar\u00e0 rimosso e saranno terminate tutte le sessioni aperte nei vari siti.
deleteAccountConfirmMessage=<br/>Si vuole veramente cancellare il proprio account? <strong>NOTA BENE: Questa azione \u00e8 irreversibile!</strong>
doDeleteConfirm=Conferma
accountDeletedMessage=L''account \u00e8 stato cancellato

View File

@ -0,0 +1,74 @@
[
{
"id": "personal-info",
"path": "personal-info",
"icon": "pf-icon-user",
"label": "personalInfoHtmlTitle",
"descriptionLabel": "personalInfoIntroMessage",
"content" : [
{
"id": "personal-info-base",
"path": "personal-info-base",
"label": "personalBasicInfoHtmlTitle",
"modulePath": "/content/account-page/AccountPage.js",
"componentName": "AccountPage"
},
{
"id": "account-extra",
"path": "account-extra",
"label": "accountExtraInfoHtmlTitleHome",
"modulePath": "/content/d4science-page/AccountExtraPage.js",
"componentName": "AccountExtraPage"
}
]
},
{
"id": "security",
"icon": "pf-icon-security",
"label": "Account Security",
"descriptionLabel": "accountSecurityIntroMessage",
"content": [
{
"id": "signingin",
"path": "security/signingin",
"label": "signingIn",
"modulePath": "/content/signingin-page/SigningInPage.js",
"componentName": "SigningInPage"
},
{
"id": "device-activity",
"path": "security/device-activity",
"label": "device-activity",
"modulePath": "/content/device-activity-page/DeviceActivityPage.js",
"componentName": "DeviceActivityPage"
},
{
"id": "linked-accounts",
"path": "security/linked-accounts",
"label": "linkedAccountsHtmlTitle",
"modulePath": "/content/linked-accounts-page/LinkedAccountsPage.js",
"componentName": "LinkedAccountsPage",
"hidden": "!features.isLinkedAccountsEnabled"
}
]
},
{
"id": "applications",
"icon": "pf-icon-applications",
"path": "applications",
"label": "applications",
"descriptionLabel": "applicationsIntroMessage",
"modulePath": "/content/applications-page/ApplicationsPage.js",
"componentName": "ApplicationsPage"
},
{
"id": "resources",
"icon": "pf-icon-repository",
"path": "resources",
"label": "resources",
"descriptionLabel": "resourceIntroMessage",
"modulePath": "/content/my-resources-page/MyResourcesPage.js",
"componentName": "MyResourcesPage",
"hidden": "!features.isMyResourcesEnabled"
}
]

View File

@ -0,0 +1,102 @@
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import * as React from "../../../../common/keycloak/web_modules/react.js";
import { Button, Grid, GridItem, Expandable, Modal } from "../../../../common/keycloak/web_modules/@patternfly/react-core.js";
import { AccountServiceContext } from "../../account-service/AccountServiceContext.js";
import { Msg } from "../../widgets/Msg.js";
import { ContentPage } from "../ContentPage.js";
import { ContentAlert } from "../ContentAlert.js";
import { AvatarForm } from "./AvatarForm.js";
export class AccountExtraPage extends React.Component {
constructor(props) {
super(props);
_defineProperty(this, "context", void 0);
_defineProperty(this, "handleModalToggle", open => {
this.setState({
isModalOpen: open
});
});
_defineProperty(this, "modalConfirmDelete", event => {
const accountUrl = this.context["accountUrl"];
const deleteUrl = accountUrl + "-delete/request-delete";
this.context.doPost(deleteUrl, {}).then(() => {
ContentAlert.success('accountDeletedMessage');
window.location.reload();
});
this.setState({
isModalOpen: false
});
});
this.state = {
isModalOpen: false
};
}
render() {
const accountUrl = this.context["accountUrl"];
return React.createElement(ContentPage, {
title: "accountExtraInfoHtmlTitle",
introMessage: "accountExtraSubMessage"
}, React.createElement(AvatarForm, {
accountUrl: accountUrl
}), React.createElement("div", {
id: "delete-account",
style: {
marginTop: "30px"
}
}, React.createElement(Expandable, {
toggleText: Msg.localize('deleteAccount')
}, React.createElement(Grid, {
gutter: "sm"
}, React.createElement(GridItem, {
span: 8
}, React.createElement("p", {
dangerouslySetInnerHTML: {
__html: Msg.localize('deleteAccountInfoMessage')
}
})), React.createElement(GridItem, {
span: 4
}, React.createElement(Button, {
id: "delete-account-btn",
variant: "danger",
onClick: e => this.handleModalToggle(true),
className: "delete-button"
}, React.createElement(Msg, {
msgKey: "doDelete"
}))))), React.createElement(Modal, {
width: '50%',
title: Msg.localize('deleteAccountDialogHeader'),
isOpen: this.state.isModalOpen,
onClose: () => this.handleModalToggle(false),
actions: [React.createElement(Button, {
key: "confirm",
variant: "danger",
onClick: this.modalConfirmDelete
}, React.createElement(Msg, {
msgKey: "doDeleteConfirm"
})), React.createElement(Button, {
key: "cancel",
variant: "secondary",
onClick: e => this.handleModalToggle(false)
}, React.createElement(Msg, {
msgKey: "doCancel"
}))]
}, React.createElement("div", {
dangerouslySetInnerHTML: {
__html: Msg.localize('deleteAccountWarningMessage')
}
}), React.createElement("div", {
dangerouslySetInnerHTML: {
__html: Msg.localize('deleteAccountConfirmMessage')
}
}))));
}
}
_defineProperty(AccountExtraPage, "contextType", AccountServiceContext);
//# sourceMappingURL=AccountExtraPage.js.map

View File

@ -0,0 +1,194 @@
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import * as React from "../../../../common/keycloak/web_modules/react.js";
import { Form, FormGroup, ActionGroup, FileUpload, Avatar, Button, Tooltip } from "../../../../common/keycloak/web_modules/@patternfly/react-core.js";
import { OutlinedQuestionCircleIcon } from "../../../../common/keycloak/web_modules/@patternfly/react-icons.js";
import { AccountServiceContext } from "../../account-service/AccountServiceContext.js";
import { ContentAlert } from "../ContentAlert.js";
import { Msg } from "../../widgets/Msg.js";
export class AvatarForm extends React.Component {
constructor(props, context) {
super(props);
_defineProperty(this, "context", void 0);
_defineProperty(this, "handleFileInputChange", void 0);
_defineProperty(this, "imageScale", (imgData, callback) => {
var img = new Image();
img.src = imgData;
img.onload = event => {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var MAX_WIDTH = 250;
var MAX_HEIGHT = 250;
var width = img.width;
var height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(callback);
};
});
_defineProperty(this, "handleSubmit", event => {
event.preventDefault();
const form = event.target;
var formData = new FormData(form);
formData.append("image", this.state.imageBlob);
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (200 <= xhr.status && xhr.status <= 204) {
ContentAlert.success('avatarUpdatedMessage');
} else {
ContentAlert.danger(xhr.response);
} // force reload avatar
this.setState({
errors: {
avatar: ""
},
imageBlob: null,
filename: "",
avatarSrc: this.state.avatarUrl
});
}
};
xhr.open(form.method, form.action, true);
xhr.send(formData);
});
_defineProperty(this, "handleError", event => {
this.setState({
errors: {
avatar: Msg.localize('error-noAvatarFound')
},
avatarSrc: ""
});
});
this.context = context;
var currentAvatar = props.accountUrl + "-avatar";
this.state = {
errors: {
avatar: ''
},
imageBlob: null,
filename: "",
avatarUrl: currentAvatar,
avatarSrc: currentAvatar,
noAvatarSrc: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMiwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzNiAzNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzYgMzY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbDojRjBGMEYwO30KCS5zdDF7ZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbDojRDJEMkQyO30KCS5zdDJ7ZmlsbDojQjhCQkJFO30KCS5zdDN7ZmlsbDojRDJEMkQyO30KPC9zdHlsZT4KPHJlY3QgY2xhc3M9InN0MCIgd2lkdGg9IjM2IiBoZWlnaHQ9IjM2Ii8+CjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xNy43LDIwLjFjLTMuNSwwLTYuNC0yLjktNi40LTYuNHMyLjktNi40LDYuNC02LjRzNi40LDIuOSw2LjQsNi40UzIxLjMsMjAuMSwxNy43LDIwLjF6Ii8+CjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xMy4zLDM2bDAtNi43Yy0yLDAuNC0yLjksMS40LTMuMSwzLjVMMTAuMSwzNkgxMy4zeiIvPgo8cGF0aCBjbGFzcz0ic3QzIiBkPSJNMTAuMSwzNmwwLjEtMy4yYzAuMi0yLjEsMS4xLTMuMSwzLjEtMy41bDAsNi43aDkuNGwwLTYuN2MyLDAuNCwyLjksMS40LDMuMSwzLjVsMC4xLDMuMmg0LjcKCWMtMC40LTMuOS0xLjMtOS0yLjktMTFjLTEuMS0xLjQtMi4zLTIuMi0zLjUtMi42cy0xLjgtMC42LTYuMy0wLjZzLTYuMSwwLjctNi4xLDAuN2MtMS4yLDAuNC0yLjQsMS4yLTMuNCwyLjYKCUM2LjcsMjcsNS44LDMyLjIsNS40LDM2SDEwLjF6Ii8+CjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0yNS45LDM2bC0wLjEtMy4yYy0wLjItMi4xLTEuMS0zLjEtMy4xLTMuNWwwLDYuN0gyNS45eiIvPgo8L3N2Zz4="
};
var reader = new FileReader();
reader.onloadend = function (event) {
var imgData = String(event.target.result);
this.imageScale(imgData, blob => {
this.setState({
imageBlob: blob,
avatarSrc: URL.createObjectURL(blob)
});
});
}.bind(this);
this.handleFileInputChange = (file, filename) => {
if (filename != "") {
this.setState({
filename: filename
});
reader.readAsDataURL(file);
} else {
this.setState({
imageBlob: null,
filename: "",
avatarSrc: currentAvatar
});
}
};
}
render() {
const {
filename,
avatarUrl,
avatarSrc,
noAvatarSrc
} = this.state;
const avatarStyle = {
objectFit: 'cover',
width: '150px',
height: '150px',
border: '1px solid lightgray',
boxShadow: 'lightgray 6px 3px 10px 2px'
};
return React.createElement(Form, {
id: "avatarForm",
method: "post",
isHorizontal: true,
action: avatarUrl,
encType: "multipart/form-data",
onSubmit: event => this.handleSubmit(event)
}, React.createElement(FormGroup, {
label: Msg.localize('avatarLabel'),
fieldId: "avatar-current-or-preview",
helperTextInvalid: this.state.errors.avatar,
isValid: this.state.errors.avatar === ''
}, avatarSrc !== "" ? React.createElement(Avatar, {
src: avatarSrc,
style: avatarStyle,
alt: "Avatar image preview",
onError: this.handleError
}) : React.createElement(Avatar, {
src: noAvatarSrc,
style: avatarStyle,
alt: "No avatar found"
})), React.createElement(FormGroup, {
fieldId: "avatar-upload",
label: React.createElement("span", null, React.createElement(Msg, {
msgKey: "uploadLabel"
}), ' ', React.createElement(Tooltip, {
content: React.createElement(Msg, {
msgKey: "avatarInfo"
})
}, React.createElement(OutlinedQuestionCircleIcon, null)))
}, React.createElement(FileUpload, {
id: "simple-file",
filename: filename,
filenamePlaceholder: Msg.localize('dragdropInfo'),
browseButtonText: Msg.localize('browseButton'),
clearButtonText: Msg.localize('clearButton'),
onChange: this.handleFileInputChange
})), React.createElement(ActionGroup, null, React.createElement(Button, {
id: "save-btn",
type: "submit",
variant: "primary",
isDisabled: filename === ""
}, React.createElement(Msg, {
msgKey: "doSave"
}))));
}
}
_defineProperty(AvatarForm, "contextType", AccountServiceContext);
//# sourceMappingURL=AvatarForm.js.map

View File

@ -0,0 +1,23 @@
body {
/* --pf-global--FontFamily--sans-serif: Comic Sans MS; */
/* --pf-global--FontFamily--heading--sans-serif: Comic Sans MS; */
--pf-global--BackgroundColor--dark-100: #303030;
--pf-global--Color--100: #151515;
}
.pf-c-nav__list .pf-c-nav__link {
--pf-c-nav__list-link--Color: #303030;
--pf-c-nav__list-link--m-current--Color: #151515;
}
.pf-c-nav__simple-list .pf-c-nav__link {
--pf-c-nav__simple-list-link--Color: #303030;
--pf-c-nav__simple-list-link--m-current--Color: #151515;
}
.brand {
height: 50px !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,88 @@
import * as React from 'react';
import { Button, Grid, GridItem, Expandable, Modal, Form } from '@patternfly/react-core';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { Msg } from '../../widgets/Msg';
import { ContentPage } from '../ContentPage';
import { ContentAlert } from '../ContentAlert';
import { AvatarForm } from './AvatarForm';
interface AccountExtraPageProps {
}
interface AccountExtraPageState {
isModalOpen: boolean;
}
export class AccountExtraPage extends React.Component<AccountExtraPageProps, AccountExtraPageState> {
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
constructor(props: any) {
super(props)
this.state = { isModalOpen: false }
}
private handleModalToggle = (open: boolean) => {
this.setState({ isModalOpen: open })
}
private modalConfirmDelete = (event: any) => {
const accountUrl = this.context!["accountUrl"]
const deleteUrl = accountUrl + "-delete/request-delete"
this.context!.doPost<void>(deleteUrl, {})
.then(() => {
ContentAlert.success('accountDeletedMessage')
window.location.reload();
})
this.setState({ isModalOpen: false })
}
public render(): React.ReactNode {
const accountUrl = this.context!["accountUrl"]
return (
<ContentPage title="accountExtraInfoHtmlTitle"
introMessage="accountExtraSubMessage"
>
<AvatarForm accountUrl={accountUrl} />
<div id="delete-account" style={{marginTop:"30px"}}>
<Expandable toggleText={Msg.localize('deleteAccount')}>
<Grid gutter={"sm"}>
<GridItem span={8}>
<p dangerouslySetInnerHTML={{ __html: Msg.localize('deleteAccountInfoMessage')}} />
</GridItem>
<GridItem span={4}>
<Button id="delete-account-btn" variant="danger"
onClick={(e) => this.handleModalToggle(true)} className="delete-button"
>
<Msg msgKey="doDelete" />
</Button>
</GridItem>
</Grid>
</Expandable>
<Modal
width={'50%'}
title={Msg.localize('deleteAccountDialogHeader')}
isOpen={this.state.isModalOpen}
onClose={() => this.handleModalToggle(false)}
actions={[
<Button key="confirm" variant="danger" onClick={this.modalConfirmDelete}>
<Msg msgKey="doDeleteConfirm" />
</Button>,
<Button key="cancel" variant="secondary" onClick={(e) => this.handleModalToggle(false)}>
<Msg msgKey="doCancel" />
</Button>
]}
>
<div dangerouslySetInnerHTML={{ __html: Msg.localize('deleteAccountWarningMessage')}} />
<div dangerouslySetInnerHTML={{ __html: Msg.localize('deleteAccountConfirmMessage')}} />
</Modal>
</div>
</ContentPage>
)
}
}

View File

@ -0,0 +1,190 @@
import * as React from 'react';
import * as CSS from 'csstype';
import { Form, FormGroup, ActionGroup, FileUpload, Avatar, Button, Tooltip } from "@patternfly/react-core";
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import { AccountServiceContext } from '../../account-service/AccountServiceContext';
import { ContentAlert } from '../ContentAlert';
import { Msg } from '../../widgets/Msg';
interface AvatarFormProps {
accountUrl: string;
}
interface AvatarFormState {
errors: any;
imageBlob: any;
filename: string;
avatarUrl: string;
avatarSrc: string;
noAvatarSrc: string;
}
export class AvatarForm extends React.Component<AvatarFormProps, AvatarFormState> {
static contextType = AccountServiceContext;
context: React.ContextType<typeof AccountServiceContext>;
private handleFileInputChange: any;
constructor(props: AvatarFormProps, context: React.ContextType<typeof AccountServiceContext>) {
super(props);
this.context = context;
var currentAvatar = props.accountUrl + "-avatar"
this.state = {
errors: {avatar: ''},
imageBlob: null,
filename: "",
avatarUrl: currentAvatar,
avatarSrc: currentAvatar,
noAvatarSrc: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMiwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzNiAzNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzYgMzY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbDojRjBGMEYwO30KCS5zdDF7ZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbDojRDJEMkQyO30KCS5zdDJ7ZmlsbDojQjhCQkJFO30KCS5zdDN7ZmlsbDojRDJEMkQyO30KPC9zdHlsZT4KPHJlY3QgY2xhc3M9InN0MCIgd2lkdGg9IjM2IiBoZWlnaHQ9IjM2Ii8+CjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xNy43LDIwLjFjLTMuNSwwLTYuNC0yLjktNi40LTYuNHMyLjktNi40LDYuNC02LjRzNi40LDIuOSw2LjQsNi40UzIxLjMsMjAuMSwxNy43LDIwLjF6Ii8+CjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xMy4zLDM2bDAtNi43Yy0yLDAuNC0yLjksMS40LTMuMSwzLjVMMTAuMSwzNkgxMy4zeiIvPgo8cGF0aCBjbGFzcz0ic3QzIiBkPSJNMTAuMSwzNmwwLjEtMy4yYzAuMi0yLjEsMS4xLTMuMSwzLjEtMy41bDAsNi43aDkuNGwwLTYuN2MyLDAuNCwyLjksMS40LDMuMSwzLjVsMC4xLDMuMmg0LjcKCWMtMC40LTMuOS0xLjMtOS0yLjktMTFjLTEuMS0xLjQtMi4zLTIuMi0zLjUtMi42cy0xLjgtMC42LTYuMy0wLjZzLTYuMSwwLjctNi4xLDAuN2MtMS4yLDAuNC0yLjQsMS4yLTMuNCwyLjYKCUM2LjcsMjcsNS44LDMyLjIsNS40LDM2SDEwLjF6Ii8+CjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0yNS45LDM2bC0wLjEtMy4yYy0wLjItMi4xLTEuMS0zLjEtMy4xLTMuNWwwLDYuN0gyNS45eiIvPgo8L3N2Zz4="
}
var reader = new FileReader()
reader.onloadend = function (event: any) {
var imgData = String(event.target!.result)
this.imageScale(imgData, (blob: Blob) => {
this.setState({
imageBlob: blob,
avatarSrc: URL.createObjectURL(blob)
})
})
}.bind(this)
this.handleFileInputChange = (file: File, filename: string) => {
if (filename != "") {
this.setState({ filename: filename })
reader.readAsDataURL(file)
} else {
this.setState({
imageBlob: null,
filename: "",
avatarSrc: currentAvatar
})
}
}
}
private imageScale = (imgData: string, callback: any) => {
var img = new Image()
img.src = imgData
img.onload = (event: Event) => {
var canvas = document.createElement("canvas")
var ctx = canvas.getContext("2d")
ctx!.drawImage(img, 0, 0)
var MAX_WIDTH = 250
var MAX_HEIGHT = 250
var width = img.width
var height = img.height
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width
width = MAX_WIDTH
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height
height = MAX_HEIGHT
}
}
canvas.width = width
canvas.height = height
ctx = canvas.getContext("2d")
ctx!.drawImage(img, 0, 0, width, height)
canvas.toBlob(callback)
}
}
private handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault()
const form = event.target as HTMLFormElement
var formData = new FormData(form)
formData.append("image", this.state.imageBlob)
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (200 <= xhr.status && xhr.status <= 204) {
ContentAlert.success('avatarUpdatedMessage')
} else {
ContentAlert.danger(xhr.response)
}
// force reload avatar
this.setState({
errors: {avatar: ""},
imageBlob: null,
filename: "",
avatarSrc: this.state.avatarUrl
})
}
}
xhr.open(form.method, form.action, true);
xhr.send(formData);
}
private handleError = (event: any) => {
this.setState({
errors: {avatar: Msg.localize('error-noAvatarFound')},
avatarSrc: ""
})
}
render() {
const { filename, avatarUrl, avatarSrc, noAvatarSrc } = this.state
const avatarStyle: CSS.Properties = {
objectFit: 'cover',
width: '150px', height: '150px',
border: '1px solid lightgray',
boxShadow: 'lightgray 6px 3px 10px 2px'
}
return (
<Form id="avatarForm" method="post" isHorizontal
action={avatarUrl} encType="multipart/form-data"
onSubmit={event => this.handleSubmit(event)}
>
<FormGroup label={Msg.localize('avatarLabel')}
fieldId="avatar-current-or-preview"
helperTextInvalid={this.state.errors.avatar}
isValid={this.state.errors.avatar === ''}
>
{ avatarSrc !== ""
? <Avatar src={avatarSrc} style={avatarStyle} alt="Avatar image preview" onError={this.handleError}/>
: <Avatar src={noAvatarSrc} style={avatarStyle} alt="No avatar found" />
}
</FormGroup>
<FormGroup
fieldId="avatar-upload"
label={<span>
<Msg msgKey="uploadLabel" />
{' '}
<Tooltip content={<Msg msgKey="avatarInfo" />}>
<OutlinedQuestionCircleIcon />
</Tooltip>
</span>}
>
<FileUpload
id="simple-file"
filename={filename}
filenamePlaceholder={Msg.localize('dragdropInfo')}
browseButtonText={Msg.localize('browseButton')}
clearButtonText={Msg.localize('clearButton')}
onChange={this.handleFileInputChange}
/>
</FormGroup>
<ActionGroup>
<Button
id="save-btn" type="submit"
variant="primary"
isDisabled={filename === ""}
>
<Msg msgKey="doSave" />
</Button>
</ActionGroup>
</Form>
)
}
}

View File

@ -0,0 +1,19 @@
# This theme will inherit everything from its parent unless
# it is overridden in the current theme.
parent=keycloak.v2
# Look at the styles.css file to see examples of using PatternFly's CSS variables
# for modifying look and feel.
styles=css/styles.css
# This is the logo in upper lefthand corner.
# It must be a relative path.
logo=/public/d4science-logo.png
# This is the link followed when clicking on the logo.
# It can be any valid URL, including an external site.
logoUrl=./
# This is the icon for the account console.
# It must be a relative path.
favIcon=/public/favicon.ico

View File

@ -1,191 +0,0 @@
<#import "template.ftl" as layout>
<@layout.mainLayout active='account' bodyClass='user'; section>
<div class="row">
<div class="col-md-10">
<h2>${msg("editAccountHtmlTitle")}</h2>
</div>
<div class="col-md-2 subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
</div>
</div>
<form action="${url.accountUrl}" class="form-horizontal" method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
<#if !realm.registrationEmailAsUsername>
<div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
<div class="col-sm-2 col-md-2">
<label for="username" class="control-label">${msg("username")}</label> <#if realm.editUsernameAllowed><span class="required">*</span></#if>
</div>
<div class="col-sm-10 col-md-10">
<input type="text" class="form-control" id="username" name="username" <#if !realm.editUsernameAllowed>disabled="disabled"</#if> value="${(account.username!'')}"/>
</div>
</div>
</#if>
<div class="form-group ${messagesPerField.printIfExists('email','has-error')}">
<div class="col-sm-2 col-md-2">
<label for="email" class="control-label">${msg("email")}</label> <span class="required">*</span>
</div>
<div class="col-sm-10 col-md-10">
<input type="text" class="form-control" id="email" name="email" autofocus value="${(account.email!'')}"/>
</div>
</div>
<div class="form-group ${messagesPerField.printIfExists('firstName','has-error')}">
<div class="col-sm-2 col-md-2">
<label for="firstName" class="control-label">${msg("firstName")}</label> <span class="required">*</span>
</div>
<div class="col-sm-10 col-md-10">
<input type="text" class="form-control" id="firstName" name="firstName" value="${(account.firstName!'')}"/>
</div>
</div>
<div class="form-group ${messagesPerField.printIfExists('lastName','has-error')}">
<div class="col-sm-2 col-md-2">
<label for="lastName" class="control-label">${msg("lastName")}</label> <span class="required">*</span>
</div>
<div class="col-sm-10 col-md-10">
<input type="text" class="form-control" id="lastName" name="lastName" value="${(account.lastName!'')}"/>
</div>
</div>
<div class="form-group">
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
<div class="">
<#if url.referrerURI??><a href="${url.referrerURI}">${kcSanitize(msg("backToApplication")?no_esc)}</a></#if>
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
</div>
</div>
</div>
</form>
<div class="row">
<div class="col-md-10">
<h2>${msg("changeAvatarHtmlTitle")}</h2>
</div>
</div>
<#assign avatarUrl = url.accountUrl?replace("^(.*)(/account/?)(\\?(.*))?$", "$1/avatar-provider/?account&$4", 'r') />
<form id="theForm" action="${avatarUrl}" class="form-horizontal" method="post" enctype="multipart/form-data">
<img src="${avatarUrl}" style="max-width: 200px;"
onerror="let div=document.createElement('div');div.id='avatarInfo';div.innerHTML='${msg("noAvatarSet")}<br/>${msg("avatarFileSizeMessage")}';this.replaceWith(div)" />
<div id="avatarError" class="alert alert-danger" style="margin-top: 0; display: none;">
<span class="pficon pficon-error-circle-o"></span>
<strong>${msg("avatarFileTooBig")}</strong> ${msg("avatarFileSizeMessage")}
</div>
<input style="margin-top: 1em;" type="file" id="avatar" name="imageSelector">
<input type="hidden" name="stateChecker" value="${stateChecker}">
<div class="form-group">
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
<div class="">
<button type="button" onClick="sendAvatar()" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
</div>
</div>
</div>
<script>
function sendAvatar() {
var filesToUpload = avatar.files;
var file = filesToUpload[0];
// Create an image
var img = document.createElement("img");
// Create a file reader
var reader = new FileReader();
// Set the image once loaded into file reader
reader.onload = function(e) {
img.src = e.target.result;
img.onload = function () {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var MAX_WIDTH = 250;
var MAX_HEIGHT = 250;
var width = img.width;
var height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(function(blob) {
var form = document.getElementById('theForm')
var formData = new FormData(form)
// Deleting the big image from the form data to prevent send
formData.delete('imageSelector')
// Adding new blob with new image with right size
formData.append("image", blob);
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
location.reload();
}
};
xhr.open(form.method, form.action, true);
xhr.send(formData);
});
} // img.onload
}
// Load files into file reader
reader.readAsDataURL(file);
}
</script>
</form>
<div class="row">
<div class="col-md-10">
<h2>${msg("deleteAccountHtmlTitle")}</h2>
</div>
</div>
<#assign deleteUrl = url.accountUrl?replace("^(.*)(/account/?)(\\?(.*))?$", "$1/delete-account/delete?$4", 'r') />
<form action="${deleteUrl}" class="form-horizontal" method="post" onsubmit="return confirm('${msg("deleteAccountConfirmDeleteMessage")}');" >
<input type="hidden" name="stateChecker" value="${stateChecker}">
<div class="form-group">
<div class="col-md-12">${msg("deleteAccountMessage")}</div>
</div>
<div class="form-group" style="border: 1px solid #f1d875; background: #fffbdc">
<div class="col-md-1" style="font-weight: bold; color: #bf7900;">${msg("deleteAccountWarningTitle")}</div>
<div class="col-md-11">${msg("deleteAccountWarningMessage")}</div>
</div>
<div class="form-group">
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
<div class="">
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="${msg("deleteAccountSubmitButton")}">${msg("deleteAccountSubmitButton")}</button>
</div>
</div>
</div>
</form>
</@layout.mainLayout>

View File

@ -1 +1 @@
parent=keycloak.v2
parent=keycloak

View File

@ -1,2 +1 @@
parent=keycloak
scripts=js/user-avatar.js
parent=keycloak

View File

@ -1 +1 @@
parent=keycloak.v2
parent=keycloak

View File

@ -1,43 +1,82 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout; section>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','email','firstName','lastName'); section>
<#if section = "header">
${msg("loginProfileTitle")}
<#elseif section = "form">
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<#if user.editUsernameAllowed>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" name="username" value="${(user.username!'')}" class="${properties.kcInputClass!}" pattern="^(?!postfix$)(?!cyrus$)[a-zA-Z0-9\.]+$" title='${msg("usernameValidityMsg")}' />
<!-- D4Science username constraints with pattern check -->
<input type="text" id="username" name="username" value="${(user.username!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
pattern="^(?!postfix$)(?!cyrus$)[a-zA-Z0-9\.]+$"
title="${msg('usernameValidityMsg')}"
/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="email" name="email" value="${(user.email!'')}" class="${properties.kcInputClass!}" />
<input type="text" id="email" name="email" value="${(user.email!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('email')>true</#if>"
/>
<#if messagesPerField.existsError('email')>
<span id="input-error-email" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('email'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('firstName',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="firstName" name="firstName" value="${(user.firstName!'')}" class="${properties.kcInputClass!}" />
<input type="text" id="firstName" name="firstName" value="${(user.firstName!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('firstName')>true</#if>"
/>
<#if messagesPerField.existsError('firstName')>
<span id="input-error-firstname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('firstName'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('lastName',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="lastName" name="lastName" value="${(user.lastName!'')}" class="${properties.kcInputClass!}" />
<input type="text" id="lastName" name="lastName" value="${(user.lastName!'')}"
class="${properties.kcInputClass!}"
aria-invalid="<#if messagesPerField.existsError('lastName')>true</#if>"
/>
<#if messagesPerField.existsError('lastName')>
<span id="input-error-lastname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('lastName'))?no_esc}
</span>
</#if>
</div>
</div>
@ -47,15 +86,45 @@
</div>
</div>
<!-- D4Science adds button name -->
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<#if isAppInitiatedAction??>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}"
name="submitBtn"
/>
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button>
<#else>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}"
name="submitBtn"
/>
</#if>
</div>
</div>
</form>
<!-- D4Science username constraints js check -->
<script type="text/javascript">
var d4Screenname = document.getElementById("username")
if (d4Screenname) {
d4Screenname.addEventListener("input", function(ev) {
if (d4Screenname.validity.patternMismatch) {
d4Screenname.setCustomValidity('${msg("usernameValidityMsg")}')
d4Screenname.reportValidity()
} else {
d4Screenname.setCustomValidity('')
d4Screenname.reportValidity()
}
})
}
var kcUpdateProfileForm = document.getElementById("kc-update-profile-form")
kcUpdateProfileForm.submitBtn.addEventListener("click", function(ev) {
if (!kcUpdateProfileForm.checkValidity()) {
ev.preventDefault()
ev.stopPropagation()
kcUpdateProfileForm.reportValidity()
}
})
</script>
</#if>
</@layout.registrationLayout>

View File

@ -1,3 +1,7 @@
loginAccountTitle=Accedi al tuo account
confirmLinkIdpReviewProfile=Rivedi le informazioni del nuovo profilo
usernameValidityMsg=Sono consentiti solo lettere, numeri e punti
usernameValidityMsg=Sono consentiti solo lettere, numeri e punti
identity-provider-login-label=Oppure accedi con

View File

@ -1,73 +1,131 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout; section>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section>
<#if section = "header">
${msg("registerTitle")}
<#elseif section = "form">
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('firstName',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="firstName" class="${properties.kcInputClass!}" name="firstName" value="${(register.formData.firstName!'')}" />
<input type="text" id="firstName" class="${properties.kcInputClass!}" name="firstName"
value="${(register.formData.firstName!'')}"
aria-invalid="<#if messagesPerField.existsError('firstName')>true</#if>"
/>
<#if messagesPerField.existsError('firstName')>
<span id="input-error-firstname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('firstName'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('lastName',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="lastName" class="${properties.kcInputClass!}" name="lastName" value="${(register.formData.lastName!'')}" />
<input type="text" id="lastName" class="${properties.kcInputClass!}" name="lastName"
value="${(register.formData.lastName!'')}"
aria-invalid="<#if messagesPerField.existsError('lastName')>true</#if>"
/>
<#if messagesPerField.existsError('lastName')>
<span id="input-error-lastname" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('lastName'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="email" class="${properties.kcInputClass!}" name="email" value="${(register.formData.email!'')}" autocomplete="email" />
<input type="text" id="email" class="${properties.kcInputClass!}" name="email"
value="${(register.formData.email!'')}" autocomplete="email"
aria-invalid="<#if messagesPerField.existsError('email')>true</#if>"
/>
<#if messagesPerField.existsError('email')>
<span id="input-error-email" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('email'))?no_esc}
</span>
</#if>
</div>
</div>
<#if !realm.registrationEmailAsUsername>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
<#if !realm.registrationEmailAsUsername>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<!-- D4Science username constraints with pattern check -->
<input type="text" id="username" class="${properties.kcInputClass!}" name="username"
value="${(register.formData.username!'')}" autocomplete="username"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
pattern="^(?!postfix$)(?!cyrus$)[a-zA-Z0-9\.]+$"
title="${msg('usernameValidityMsg')}"
/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" class="${properties.kcInputClass!}" name="username" value="${(register.formData.username!'')}" pattern="^(?!postfix$)(?!cyrus$)[a-zA-Z0-9\.]+$" title='${msg("usernameValidityMsg")}' autocomplete="username" />
</div>
</div>
</#if>
</#if>
<#if passwordRequired??>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('password',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="password" id="password" class="${properties.kcInputClass!}" name="password" autocomplete="new-password"/>
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="password" id="password" class="${properties.kcInputClass!}" name="password"
autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
/>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('password-confirm',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password-confirm" class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label>
<#if messagesPerField.existsError('password')>
<span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="password" id="password-confirm" class="${properties.kcInputClass!}" name="password-confirm" />
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password-confirm"
class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="password" id="password-confirm" class="${properties.kcInputClass!}"
name="password-confirm"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"
/>
<#if messagesPerField.existsError('password-confirm')>
<span id="input-error-password-confirm" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
</span>
</#if>
</div>
</div>
</div>
</#if>
<#if recaptchaRequired??>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
<div class="g-recaptcha" data-size="compact" data-sitekey="${recaptchaSiteKey}"></div>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
<div class="g-recaptcha" data-size="compact" data-sitekey="${recaptchaSiteKey}"></div>
</div>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}">
@ -78,21 +136,25 @@
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="submitBtn" type="submit" value="${msg("doRegister")}"/>
<!-- D4Science adds button name -->
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doRegister")}"
name="submitBtn"
/>
</div>
</div>
</form>
<!-- D4Science username constraints js check -->
<script type="text/javascript">
var screenname = document.getElementById("username")
if (screenname) {
screenname.addEventListener("input", function(ev) {
if (screenname.validity.patternMismatch) {
screenname.setCustomValidity('${msg("usernameValidityMsg")}')
screenname.reportValidity()
var d4Screenname = document.getElementById("username")
if (d4Screenname) {
d4Screenname.addEventListener("input", function(ev) {
if (d4Screenname.validity.patternMismatch) {
d4Screenname.setCustomValidity('${msg("usernameValidityMsg")}')
d4Screenname.reportValidity()
} else {
screenname.setCustomValidity('')
screenname.reportValidity()
d4Screenname.setCustomValidity('')
d4Screenname.reportValidity()
}
})
}
@ -107,4 +169,4 @@
})
</script>
</#if>
</@layout.registrationLayout>
</@layout.registrationLayout>

View File

@ -1,568 +1,29 @@
.login-pf body {
display: flex;
flex-direction: column;
height: 100vh;
background: url("../img/color-triangles.png") no-repeat center center fixed;
background-size: cover;
height: 100%;
}
#content {
flex: 1 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
.login-pf-page {
padding-top: 30px;
padding-bottom: 30px;
}
.alert-error {
background-color: #ffffff;
border-color: #cc0000;
color: #333333;
}
#kc-locale ul {
display: none;
position: absolute;
background-color: #fff;
list-style: none;
right: 0;
top: 20px;
min-width: 100px;
padding: 2px 0;
border: solid 1px #bbb;
}
#kc-locale:hover ul {
display: block;
margin: 0;
}
#kc-locale ul li a {
display: block;
padding: 5px 14px;
color: #000 !important;
text-decoration: none;
line-height: 20px;
}
#kc-locale ul li a:hover {
color: #4d5258;
background-color: #d4edfa;
}
#kc-locale-dropdown a {
color: #4d5258;
background: 0 0;
padding: 0 15px 0 0;
font-weight: 300;
}
#kc-locale-dropdown a:hover {
text-decoration: none;
}
a#kc-current-locale-link {
display: block;
padding: 0 5px;
}
/* a#kc-current-locale-link:hover {
background-color: rgba(0,0,0,0.2);
} */
a#kc-current-locale-link::after {
content: "\2c5";
margin-left: 4px;
}
.login-pf .container {
padding-top: 40px;
}
.login-pf a:hover {
color: #0099d3;
}
#kc-logo {
width: 100%;
}
#kc-logo-wrapper {
background-image: url(../img/keycloak-logo-2.png);
background-repeat: no-repeat;
height: 63px;
width: 300px;
margin: 62px auto 0;
}
div.kc-logo-text {
background-image: url(../img/keycloak-logo-text.png);
background-repeat: no-repeat;
height: 63px;
width: 300px;
margin: 0 auto;
}
div.kc-logo-text span {
display: none;
}
#kc-header {
color: #ededed;
overflow: visible;
white-space: nowrap;
}
#kc-header-wrapper {
font-size: 29px;
text-transform: uppercase;
letter-spacing: 3px;
line-height: 1.2em;
padding: 62px 10px 20px;
white-space: normal;
}
#kc-content {
width: 100%;
}
#kc-attempted-username{
font-size: 20px;
font-family:inherit;
font-weight: normal;
padding-right:10px;
}
#kc-username{
text-align: center;
}
#kc-webauthn-settings-form{
padding-top:8px;
}
/* #kc-content-wrapper {
overflow-y: hidden;
} */
/* #kc-info {
padding-bottom: 200px;
margin-bottom: -200px;
} */
#kc-info-wrapper {
font-size: 13px;
}
#kc-form-options span {
display: block;
}
#kc-form-options .checkbox {
margin-top: 0;
color: #72767b;
}
#kc-terms-text {
margin-bottom: 20px;
}
#kc-registration {
margin-bottom: 15px;
}
/* TOTP */
.subtitle {
text-align: right;
margin-top: 30px;
color: #909090;
}
.required {
color: #CB2915;
}
ol#kc-totp-settings {
margin: 0;
padding-left: 20px;
}
ul#kc-totp-supported-apps {
margin-bottom: 10px;
}
#kc-totp-secret-qr-code {
max-width:150px;
max-height:150px;
}
#kc-totp-secret-key {
background-color: #fff;
color: #333333;
font-size: 16px;
padding: 10px 0;
}
/* OAuth */
#kc-oauth h3 {
margin-top: 0;
}
#kc-oauth ul {
list-style: none;
padding: 0;
margin: 0;
}
#kc-oauth ul li {
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 12px;
padding: 10px 0;
}
#kc-oauth ul li:first-of-type {
border-top: 0;
}
#kc-oauth .kc-role {
display: inline-block;
width: 50%;
}
/* Code */
#kc-code textarea {
width: 100%;
height: 8em;
}
/* Social */
#kc-social-providers ul {
padding: 0;
}
#kc-social-providers li {
display: block;
}
#kc-social-providers li:first-of-type {
margin-top: 0;
}
.kc-login-tooltip{
position:relative;
display: inline-block;
}
.kc-login-tooltip .kc-tooltip-text{
top:-3px;
left:160%;
background-color: black;
visibility: hidden;
color: #fff;
min-width:130px;
text-align: center;
border-radius: 2px;
box-shadow:0 1px 8px rgba(0,0,0,0.6);
padding: 5px;
position: absolute;
opacity:0;
transition:opacity 0.5s;
}
/* Show tooltip */
.kc-login-tooltip:hover .kc-tooltip-text {
visibility: visible;
opacity:0.7;
}
/* Arrow for tooltip */
.kc-login-tooltip .kc-tooltip-text::after {
content: " ";
position: absolute;
top: 15px;
right: 100%;
margin-top: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent black transparent transparent;
}
.zocial,
a.zocial {
width: 100%;
font-weight: normal;
font-size: 14px;
text-shadow: none;
border: 0;
background: #f5f5f5;
color: #72767b;
border-radius: 0;
white-space: normal;
}
.zocial:before {
border-right: 0;
margin-right: 0;
}
.zocial span:before {
padding: 7px 10px;
font-size: 14px;
}
.zocial:hover {
background: #ededed !important;
}
.zocial.facebook,
.zocial.github,
.zocial.google,
.zocial.microsoft,
.zocial.stackoverflow,
.zocial.linkedin,
.zocial.twitter {
background-image: none;
border: 0;
box-shadow: none;
text-shadow: none;
}
/* Copy of zocial windows classes to be used for microsoft's social provider button */
.zocial.microsoft:before{ content: "\f15d"; }
.zocial.stackoverflow:before{ color: inherit; }
.zocial.oidc:before{
content: " ";
background: url(../img/academic.png);
height: 25px;
width: 25px;
background-size: contain;
background-repeat: no-repeat;
margin: 3px 3px 3px 7px;
}
.zocial.google:before{
content: " ";
background: url(../img/google.png);
height: 16px;
width: 16px;
background-size: contain;
background-repeat: no-repeat;
margin: 6px 8px 8px 8px;
}
@media (min-width: 768px) {
#kc-container-wrapper {
position: absolute;
width: 100%;
}
.login-pf .container {
padding-right: 80px;
}
#kc-locale {
position: relative;
text-align: right;
z-index: 9999;
}
}
@media (max-width: 767px) {
.login-pf body {
background: white;
}
#kc-header {
padding-left: 15px;
padding-right: 15px;
float: none;
text-align: left;
}
#kc-header-wrapper {
font-size: 16px;
font-weight: bold;
padding: 20px 60px 0 0;
color: #72767b;
letter-spacing: 0;
}
div.kc-logo-text {
margin: 0;
width: 150px;
height: 32px;
background-size: 100%;
}
#kc-form {
float: none;
}
#kc-info-wrapper {
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 15px;
padding-top: 15px;
padding-left: 0px;
padding-right: 15px;
}
#kc-social-providers li {
display: block;
margin-right: 5px;
}
.login-pf .container {
padding-top: 15px;
padding-bottom: 15px;
}
#kc-locale {
position: absolute;
width: 200px;
top: 20px;
right: 20px;
text-align: right;
z-index: 9999;
}
#kc-logo-wrapper {
background-size: 100px 21px;
height: 21px;
width: 100px;
margin: 20px 0 0 20px;
}
}
@media (min-height: 646px) {
#kc-container-wrapper {
bottom: 12%;
}
}
@media (max-height: 645px) {
#kc-container-wrapper {
padding-top: 50px;
top: 20%;
}
}
.card-pf form.form-actions .btn {
float: right;
margin-left: 10px;
}
#kc-form-buttons {
margin-top: 40px;
}
.login-pf-page .login-pf-brand {
margin-top: 20px;
max-width: 360px;
width: 40%;
}
.card-pf {
background: #fff;
margin: 0 auto;
padding: 0 20px;
max-width: 500px;
border-top: 0;
box-shadow: 0 0 0;
}
.login-pf-page .card-pf{
padding: 20px 20px 20px 20px;
}
/*tablet*/
@media (max-width: 840px) {
.login-pf-page .card-pf{
max-width: none;
margin-left: 20px;
margin-right: 20px;
padding: 20px 20px 20px 20px;
}
}
@media (max-width: 767px) {
.login-pf-page .card-pf{
max-width: none;
margin-left: 0;
margin-right: 0;
padding-top: 0;
}
.card-pf.login-pf-accounts{
max-width: none;
}
}
.login-pf-page .login-pf-signup {
font-size: 15px;
color: #72767b;
}
#kc-content-wrapper .row {
margin-left: 0;
margin-right: 0;
}
@media (min-width: 768px) {
.login-pf-page .login-pf-social-section:first-of-type {
padding-right: 39px;
border-right: 1px solid #d1d1d1;
margin-right: -1px;
}
.login-pf-page .login-pf-social-section:last-of-type {
padding-left: 40px;
}
.login-pf-page .login-pf-social-section .login-pf-social-link:last-of-type {
#kc-info {
margin-bottom: 0;
}
.login-pf-page .login-pf-social-double-col .login-pf-social-link {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
}
}
.login-pf-page .login-pf-social-link {
margin-bottom: 20px;
}
.login-pf-page .login-pf-social-link a {
padding: 2px 0;
.card-d4s-wide {
max-width: 750px;
}
.login-pf-page.login-pf-page-accounts {
margin-left: auto;
margin-right: auto;
.d4s-terms-text {
max-height: 40vh;
overflow-y: auto;
padding: 0 20px 10px 0;
}
.login-pf-page .btn-primary {
margin-top: 0;
}
.login-pf-page .list-view-pf .list-group-item {
border-bottom: 1px solid #ededed;
}
.login-pf-page .list-view-pf-description {
width: 100%;
}
.login-pf-page .card-pf{
margin-bottom: 10px;
}
#kc-form-login div.form-group:last-of-type,
#kc-register-form div.form-group:last-of-type,
#kc-update-profile-form div.form-group:last-of-type {
margin-bottom: 0px;
}
#kc-back {
margin-top: 5px;
}
form#kc-select-back-form div.login-pf-social-section {
padding-left: 0px;
border-left: 0px;
.d4s-terms-info {
font-weight: bold;
font-variant: small-caps;
}

View File

@ -1,19 +1,25 @@
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false displayWide=false showAnotherWayIfPresent=true>
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false showAnotherWayIfPresent=true displayWide=false>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" class="${properties.kcHtmlClass!}">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="noindex, nofollow"><!-- base -->
<meta name="robots" content="noindex, nofollow">
<#if properties.meta?has_content>
<#list properties.meta?split(' ') as meta>
<meta name="${meta?split('==')[0]}" content="${meta?split('==')[1]}"/>
</#list>
</#if>
<!-- D4Science custom page title and favicon -->
<title><#if properties.titleTag?has_content>${properties.titleTag}<#else>${msg("loginTitle",(realm.displayName!''))}</#if></title>
<link rel="icon" href="<#if properties.favicon?has_content>${properties.favicon}<#else>${url.resourcesPath}/img/favicon.ico</#if>" />
<#if properties.stylesCommon?has_content>
<#list properties.stylesCommon?split(' ') as style>
<link href="${url.resourcesCommonPath}/${style}" rel="stylesheet" />
</#list>
</#if>
<#if properties.styles?has_content>
<#list properties.styles?split(' ') as style>
<link href="${url.resourcesPath}/${style}" rel="stylesheet" />
@ -31,80 +37,82 @@
</#if>
</head>
<body class="${properties.kcBodyClass!}">
<div id="content" style="<#if properties.contentBgImg?has_content>background: url('${url.resourcesPath}/${properties.contentBgImg!}') no-repeat center center fixed; background-size: cover;</#if> ${properties.contentStyle!}">
<div class="${properties.kcLoginClass!}">
<!--
<!-- D4Science body style overridden to handle bg image custom configs -->
<body class="${properties.kcBodyClass!}"
style="<#if properties.contentBgImg?has_content>background: url('${url.resourcesPath}/${properties.contentBgImg!}') no-repeat center center fixed; background-size: cover; height: 100%;</#if> ${properties.contentStyle!}">
<div class="${properties.kcLoginClass!}">
<!-- D4Science - does not want this header
<div id="kc-header" class="${properties.kcHeaderClass!}">
<div id="kc-header-wrapper" class="${properties.kcHeaderWrapperClass!}">${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}</div>
<div id="kc-header-wrapper"
class="${properties.kcHeaderWrapperClass!}">${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}</div>
</div>
-->
<div class="${properties.kcFormCardClass!} <#if displayWide>${properties.kcFormCardAccountClass!}</#if>">
<div class="${properties.kcFormCardClass!} <#if displayWide>${properties.kcFormCardWideClass!}</#if>">
<div style="${properties.logoHeaderStyle!}">
<div>
<#if client?? && client.getClientId()!='account'>
<a href="<#if client.getBaseUrl()?has_content>${client.getBaseUrl()}<#else>${"https://" + client.getClientId()}</#if>">
<#if properties.logoSrc?has_content>
<img class="img-fluid float-left" alt="${properties.logoAlt!}" src="${properties.logoSrc!}" style="${properties.logoStyle!}">
<#else>
<h1>
<#if client.getName()?has_content && client.getName()?starts_with("${")>${kcSanitize(msg(client.getName()?keep_after("{")?keep_before("}")))}
<#else>${(client.getName()!client.getClientId()!"undefined client")?capitalize?keep_before('.')}</#if>
</h1>
</#if>
</a>
</#if>
</div>
<#if properties.infrastructureLogo?has_content && properties.infrastructureLogo='yes'>
<div>
<a target="_blank" href="http://www.d4science.org">
<img class="img-fluid float-right" alt="D4Science Infrastructure" src="${url.resourcesPath}/img/PoweredByD4Science.png" style="${properties.infrastructureLogoStyle!}">
</a>
</div>
</#if>
</div>
<header class="${properties.kcFormHeaderClass!}">
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
<div id="kc-locale">
<div id="kc-locale-wrapper" class="${properties.kcLocaleWrapperClass!}">
<div class="kc-dropdown" id="kc-locale-dropdown">
<a href="#" id="kc-current-locale-link">${locale.current}</a>
<ul>
<#list locale.supported as l>
<li class="kc-dropdown-item"><a href="${l.url}">${l.label}</a></li>
</#list>
</ul>
</div>
</div>
<!-- D4Science logos header inside formCard -->
<div style="${properties.logoHeaderStyle!}">
<div>
<#if client?? && client.getClientId()!='account'>
<a href="<#if client.getBaseUrl()?has_content>${client.getBaseUrl()}<#else>${"https://" + client.getClientId()}</#if>">
<#if properties.logoSrc?has_content>
<img class="img-fluid float-left" alt="${properties.logoAlt!}" src="${properties.logoSrc!}" style="${properties.logoStyle!}">
<#else>
<h1>
<#if client.getName()?has_content && client.getName()?starts_with("${")>${kcSanitize(msg(client.getName()?keep_after("{")?keep_before("}")))}
<#else>${(client.getName()!client.getClientId()!"undefined client")?capitalize?keep_before('.')}</#if>
</h1>
</#if>
</a>
</#if>
</div>
</#if>
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#if displayRequiredFields>
<div class="${properties.kcContentWrapperClass!}">
<div class="${properties.kcLabelWrapperClass!} subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
</div>
<div class="col-md-10">
<h1 id="kc-page-title"><#nested "header"></h1>
<#if properties.infrastructureLogo?has_content && properties.infrastructureLogo='yes'>
<div>
<a target="_blank" href="http://www.d4science.org">
<img class="img-fluid float-right" alt="D4Science Infrastructure" src="${url.resourcesPath}/img/PoweredByD4Science.png" style="${properties.infrastructureLogoStyle!}">
</a>
</div>
</#if>
</div>
<header class="${properties.kcFormHeaderClass!}">
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
<div class="${properties.kcLocaleMainClass!}" id="kc-locale">
<div id="kc-locale-wrapper" class="${properties.kcLocaleWrapperClass!}">
<div id="kc-locale-dropdown" class="${properties.kcLocaleDropDownClass!}">
<a href="#" id="kc-current-locale-link">${locale.current}</a>
<ul class="${properties.kcLocaleListClass!}">
<#list locale.supported as l>
<li class="${properties.kcLocaleListItemClass!}">
<a class="${properties.kcLocaleItemClass!}" href="${l.url}">${l.label}</a>
</li>
</#list>
</ul>
</div>
</div>
</div>
<#else>
<h1 id="kc-page-title"><#nested "header"></h1>
</#if>
<#else>
<#if displayRequiredFields>
<div class="${properties.kcContentWrapperClass!}">
<div class="${properties.kcLabelWrapperClass!} subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#if displayRequiredFields>
<div class="${properties.kcContentWrapperClass!}">
<div class="${properties.kcLabelWrapperClass!} subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
</div>
<div class="col-md-10">
<h1 id="kc-page-title"><#nested "header"></h1>
</div>
</div>
<div class="col-md-10">
<#nested "show-username">
<div class="${properties.kcFormGroupClass!}">
<div id="kc-username">
<#else>
<h1 id="kc-page-title"><#nested "header"></h1>
</#if>
<#else>
<#if displayRequiredFields>
<div class="${properties.kcContentWrapperClass!}">
<div class="${properties.kcLabelWrapperClass!} subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
</div>
<div class="col-md-10">
<#nested "show-username">
<div id="kc-username" class="${properties.kcFormGroupClass!}">
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
<a id="reset-login" href="${url.loginRestartFlowUrl}">
<div class="kc-login-tooltip">
@ -115,11 +123,9 @@
</div>
</div>
</div>
</div>
<#else>
<#nested "show-username">
<div class="${properties.kcFormGroupClass!}">
<div id="kc-username">
<#else>
<#nested "show-username">
<div id="kc-username" class="${properties.kcFormGroupClass!}">
<label id="kc-attempted-username">${auth.attemptedUsername}</label>
<a id="reset-login" href="${url.loginRestartFlowUrl}">
<div class="kc-login-tooltip">
@ -128,74 +134,74 @@
</div>
</a>
</div>
</div>
</#if>
</#if>
</#if>
</header>
<div id="kc-content">
<div id="kc-content-wrapper">
</header>
<#-- App-initiated actions should not see warning messages about the need to complete the action -->
<#-- during login. -->
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
<div class="alert alert-${message.type}">
<#if message.type = 'success'><span class="${properties.kcFeedbackSuccessIcon!}"></span></#if>
<#if message.type = 'warning'><span class="${properties.kcFeedbackWarningIcon!}"></span></#if>
<#if message.type = 'error'><span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
<#if message.type = 'info'><span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
<span class="kc-feedback-text">${kcSanitize(message.summary)?no_esc}</span>
</div>
</#if>
<div id="kc-content">
<div id="kc-content-wrapper">
<#nested "form">
<#-- App-initiated actions should not see warning messages about the need to complete the action -->
<#-- during login. -->
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
<div class="alert-${message.type} ${properties.kcAlertClass!} pf-m-<#if message.type = 'error'>danger<#else>${message.type}</#if>">
<div class="pf-c-alert__icon">
<#if message.type = 'success'><span class="${properties.kcFeedbackSuccessIcon!}"></span></#if>
<#if message.type = 'warning'><span class="${properties.kcFeedbackWarningIcon!}"></span></#if>
<#if message.type = 'error'><span class="${properties.kcFeedbackErrorIcon!}"></span></#if>
<#if message.type = 'info'><span class="${properties.kcFeedbackInfoIcon!}"></span></#if>
</div>
<span class="${properties.kcAlertTitleClass!}">${kcSanitize(message.summary)?no_esc}</span>
</div>
</#if>
<#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent>
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post" <#if displayWide>class="${properties.kcContentWrapperClass!}"</#if>>
<div <#if displayWide>class="${properties.kcFormSocialAccountContentClass!} ${properties.kcFormSocialAccountClass!}"</#if>>
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" name="tryAnotherWay" value="on" />
<a href="#" id="try-another-way" onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a>
</div>
</div>
</form>
</#if>
<!-- D4Science: reintroduces displayWide=true for terms -->
<#nested "form">
<#if displayInfo>
<div id="kc-info" class="${properties.kcSignUpClass!}">
<div id="kc-info-wrapper" class="${properties.kcInfoAreaWrapperClass!}">
<#nested "info">
</div>
</div>
</#if>
</div>
<#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent>
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" name="tryAnotherWay" value="on"/>
<a href="#" id="try-another-way"
onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a>
</div>
</form>
</#if>
</div>
<footer style="${properties.footerStyle!}">
<div>
<a href="${properties.linkTerms!}">Terms of Use</a> |
<a href="${properties.linkCookies!'#'}">Cookies Policy</a> |
<a href="${properties.linkPrivacy!'#'}" target="_blank">Privacy Policy</a> |
<#if properties.linkProject?has_content><a href="${properties.linkProject!}" target="_blank">${properties.descrProject!}</a></#if>
</div>
<#if properties.ECLogo?has_content && properties.ECLogo='yes'>
<div style="display: flex; padding-top: 10px; justify-content: space-between;">
<#if properties.footerRow?has_content><div>${kcSanitize(properties.footerRow)?no_esc}</div></#if>
<div style="align-self: center;">
<a href="http://ec.europa.eu/programmes/horizon2020/" target="_blank">
<img class="float-right" alt="${properties.ECLogoAlt!}" src="${url.resourcesPath}/img/logo-ec.jpg" style="${properties.ECLogoStyle!}">
</a>
<#if displayInfo>
<div id="kc-info" class="${properties.kcSignUpClass!}">
<div id="kc-info-wrapper" class="${properties.kcInfoAreaWrapperClass!}">
<#nested "info">
</div>
</div>
</#if>
</div>
</div>
<#else>
<#if properties.footerRow?has_content><div style="padding-top: 10px;">${kcSanitize(properties.footerRow)?no_esc}</div></#if>
</#if>
</footer>
</div>
</div><!-- end form card -->
</div><!-- end login -->
<!-- D4Science footer -->
<footer style="${properties.footerStyle!}">
<div>
<a href="${properties.linkTerms!}">Terms of Use</a> |
<a href="${properties.linkCookies!'#'}">Cookies Policy</a> |
<a href="${properties.linkPrivacy!'#'}" target="_blank">Privacy Policy</a> |
<#if properties.linkProject?has_content><a href="${properties.linkProject!}" target="_blank">${properties.descrProject!}</a></#if>
</div>
<#if properties.ECLogo?has_content && properties.ECLogo='yes'>
<div style="display: flex; padding-top: 10px; justify-content: space-between;">
<#if properties.footerRow?has_content><div>${kcSanitize(properties.footerRow)?no_esc}</div></#if>
<div style="align-self: center;">
<a href="http://ec.europa.eu/programmes/horizon2020/" target="_blank">
<img class="float-right" alt="${properties.ECLogoAlt!}" src="${url.resourcesPath}/img/logo-ec.jpg" style="${properties.ECLogoStyle!}">
</a>
</div>
</div>
<#else>
<#if properties.footerRow?has_content><div style="padding-top: 10px;">${kcSanitize(properties.footerRow)?no_esc}</div></#if>
</#if>
</footer>
</div><!-- end content -->
</div>
</div>
</body>
</html>
</#macro>

View File

@ -3,24 +3,27 @@
<#if section = "header">
${msg("termsTitle")}
<#elseif section = "form">
<div id="kc-terms-text" style="max-height: 40vh; overflow-y: auto; padding: 0 10px 10px 0;">
<#include "terms.html" parse=false>
</div>
<div style="text-align: right; font-weight: bold; font-variant: small-caps;"><p>${msg("termsAcceptMsg")}</p></div>
<form class="form-actions" action="${url.loginAction}" method="POST">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-accept" type="submit" value="${msg("doAccept")}" disabled />
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-decline" type="submit" value="${msg("doDecline")}"/>
</form>
<div class="clearfix"></div>
<script type="text/javascript">
document.getElementById("kc-terms-text").addEventListener("scroll", checkKcTermsScrollHeight, false);
<div id="kc-terms-text" class="d4s-terms-text">
<!-- D4Science terms and conditions ref -->
<#include "terms.html" parse=false>
</div>
<div class="d4s-terms-info"><p>${msg("termsAcceptMsg")}</p></div>
<form class="form-actions" action="${url.loginAction}" method="POST">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-accept" type="submit" value="${msg("doAccept")}" disabled />
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-decline" type="submit" value="${msg("doDecline")}"/>
</form>
<div class="clearfix"></div>
function checkKcTermsScrollHeight(){
<!-- D4Science enable "Accept" button -->
<script type="text/javascript">
document.getElementById("kc-terms-text").addEventListener("scroll", checkKcTermsScrollHeight, false);
function checkKcTermsScrollHeight() {
var kcTermsTextElement = document.getElementById("kc-terms-text")
if ((kcTermsTextElement.scrollTop + kcTermsTextElement.offsetHeight + 5) >= kcTermsTextElement.scrollHeight){
document.getElementById("kc-accept").disabled = false;
if ((kcTermsTextElement.scrollTop + kcTermsTextElement.offsetHeight + 10) >= kcTermsTextElement.scrollHeight){
document.getElementById("kc-accept").disabled = false;
}
}
</script>
}
</script>
</#if>
</@layout.registrationLayout>

View File

@ -1,7 +1,9 @@
parent=keycloak
import=common/keycloak
styles=node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/zocial/zocial.css css/d4science.css
#styles=node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/zocial/zocial.css css/d4science.css
styles=css/login.css css/tile.css css/d4science.css
#stylesCommon=web_modules/@patternfly/react-core/dist/styles/base.css web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css
#titleTag=
#favicon=https://services.d4science.org/favicon.ico
@ -31,4 +33,5 @@ ECLogoAlt=EU H2020 programme
footerRow=Project has received funding from the European Union's Horizon programme ...
#kcLogoIdP-eosc-oidc=
kcFormCardWideClass=card-d4s-wide
kcLogoIdP-eosc-oidc=fa fa-university

View File

@ -0,0 +1,6 @@
{
"themes": [{
"name" : "${theme-name}",
"types": [ "login" ]
}]
}

View File

@ -0,0 +1,33 @@
import module namespace c = 'urn:nubisware:keycloak:clients' at 'keycloak-clients.xqm';
import module namespace gw = 'urn:nubisware:d4science:gateways' at 'd4s-gateways.xqm';
declare function local:build-clientId($gateway) {
replace($gateway, 'https://', '')
};
declare function local:build-client-def($gateway) {
let $clientId := local:build-clientId($gateway)
let $baseUrl := $gateway || '/'
let $redirectUri := $gateway || '/*'
let $login_theme := "d4science"
return map {
"clientId" : $clientId,
"baseUrl" : $baseUrl,
"redirectUri" : $redirectUri,
"login_theme" : $login_theme
}
};
let $dummy := ''
(: delete all clients
gw:list() ! c:delete-client(local:build-clientId(.))
:)
(: add all clients
gw:list() ! c:create-client(local:build-client-def(.))
:)
return gw:list() ! c:create-client(local:build-client-def(.))

View File

@ -0,0 +1,10 @@
module namespace gw = 'urn:nubisware:d4science:gateways';
declare variable $gw:url := 'https://services.d4science.org/thematic-gateways';
declare function gw:list() {
let $req := <http:request method="get" />
return http:send-request($req, $gw:url)//a[@class='entry-link']/@href/data() ! replace(., '/explore', '')
};

View File

@ -0,0 +1,2 @@
http:send-request(<http:request href="https://services.d4science.org/thematic-gateways" method="get" />)//a[@class='entry-link']/@href/data() ! replace(., '/explore', '')

View File

@ -0,0 +1,91 @@
module namespace c = 'urn:nubisware:keycloak:clients';
declare variable $c:keycloak-url := 'http://localhost:8080';
declare variable $c:token-url := '/auth/realms/master/protocol/openid-connect/token';
declare variable $c:clients-url := '/auth/admin/realms/test/clients';
declare function c:get-token() {
let $url := $c:keycloak-url || $c:token-url
let $form-params := map {
'grant_type': 'password',
'client_id': 'admin-cli',
'username': 'admin',
'password': 'admin'
}
let $body := substring(web:create-url('', $form-params), 2)
let $http-req :=
<http:request method="GET">
<http:body media-type="application/x-www-form-urlencoded"/>
</http:request>
let $token := http:send-request($http-req, $url, $body)[2]//access__token/data()
return $token
};
declare function c:create-client($params) {
let $client-def := ``[
{
"clientId": "`{$params?clientId}`",
"baseUrl": "`{$params?baseUrl}`",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"`{$params?redirectUri}`"
],
"webOrigins": [
"/*"
],
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"publicClient": true,
"protocol": "openid-connect",
"attributes": {
"login_theme": "`{$params?login_theme}`"
},
"fullScopeAllowed": true
}
]``
let $token := c:get-token()
let $url := $c:keycloak-url || $c:clients-url
let $http-req :=
<http:request method="POST">
<http:header name="Authorization" value="Bearer {$token}"/>
<http:body media-type="application/json"/>
</http:request>
let $resp := http:send-request($http-req, $url, $client-def)
return $params?clientId || ' : ' || $resp[1]/@status/data() || ' ' || $resp[1]/@message/data()
};
declare function c:delete-client($clientId) {
let $token := c:get-token()
let $url := $c:keycloak-url || $c:clients-url
let $geturl := web:create-url($url, map { "clientId" : $clientId })
let $http-req :=
<http:request method="GET">
<http:header name="Authorization" value="Bearer {$token}"/>
</http:request>
let $resp := http:send-request($http-req, $geturl)
let $id := $resp[2]/json/_[clientId=$clientId]/id/data()
return if ($id) then
let $delurl := $url || '/' || $id
let $http-req :=
<http:request method="DELETE">
<http:header name="Authorization" value="Bearer {$token}"/>
</http:request>
let $resp := http:send-request($http-req, $delurl)
return $clientId || ' : ' || $resp[1]/@status/data() || ' ' || $resp[1]/@message/data()
};

View File

@ -0,0 +1,94 @@
declare variable $url := 'https://services.d4science.org/thematic-gateways';
declare function local:list() {
let $req := <http:request method="get" />
return http:send-request($req, $url)//a[@class='entry-link']/@href/data() ! replace(., '/explore', '')
};
declare %basex:inline function local:get-title($page){
substring-before($page/html/head/title, " -")
};
declare %basex:inline function local:get-favicon-url($page){
$page/html/head/link[@rel="Shortcut Icon"]/string(@href)
};
declare %basex:inline function local:get-background-image($page){
()
};
declare %basex:inline function local:get-logo-url($page){
$page/html/body//h1[contains(@class, "site-title")]//img/string(@src)
};
declare %basex:inline function local:get-logo-alt($page){
$page/html/body//h1[contains(@class, "site-title")]//img/string(@alt)
};
declare %basex:inline function local:get-infrastructure-logo($page){
if(exists($page/html/body//div[contains(@class,"poweredBy-link")]//img)) then "yes" else "no"
};
declare %basex:inline function local:get-terms-url($page){
$page/html/body//footer//a[@href = "/terms-of-use"]/@href
};
declare %basex:inline function local:get-cookiepolicy-url($page){
$page/html/body//footer//a[@href = "/cookie-policy"]/@href
};
declare %basex:inline function local:get-privacypolicy-url($page){
$page/html/body//footer//a[text() = "Privacy Policy"]/@href
};
declare %basex:inline function local:get-project-url($page){
$page/html/body//footer/div[contains(@class, "custom-footer-container")]/a[last()]/@href
};
declare %basex:inline function local:get-project-description($page){
()
};
declare %basex:inline function local:get-ec-logo($page){
if(exists(
$page/html/body//footer/div[not(contains(@class, "custom-footer-container"))]/a[@href = "http://ec.europa.eu/programmes/horizon2020/"]))
then "yes" else "no"
};
declare %basex:inline function local:get-footer($page){
string-join($page/html/body/footer/div/div//text() ! replace(., "'", "&quot;"), "<br/>")
};
declare %basex:inline function local:to-ansible-call($params as map(*)){
string-join(("./keycloak-action.sh inject-theme",
map:keys($params) ! ("-e '" || . || "=&quot;" || $params(.) || "&quot;'")
), " ")
};
declare %basex:inline function local:transform($page-addr){
let $page := html:parse(fetch:text($page-addr))
return
local:to-ansible-call(
map{
"theme" : substring-after($page-addr, "://"),
"title_tag" : local:get-title($page),
"favicon_url" : local:get-favicon-url($page),
"logo_url" : $page-addr || local:get-logo-url($page),
"logo_alt" : local:get-logo-alt($page),
"infrastructure_logo" : local:get-infrastructure-logo($page),
"background_image" : local:get-background-image($page),
"terms_url" : $page-addr || local:get-terms-url($page),
"cookie_policy_url" : $page-addr || local:get-cookiepolicy-url($page),
"privacy_policy_url" : "" || local:get-privacypolicy-url($page),
"project_url" : "" || local:get-project-url($page),
"project_description" : local:get-project-description($page),
"EC_logo" : local:get-ec-logo($page),
"footer" : local:get-footer($page)
}
)
};
for $page-addr in local:list()
return xquery:fork-join(function(){ local:transform($page-addr)})