Skip to content

Commit

Permalink
Merge pull request #119 from City-of-Helsinki/release/1.1.0
Browse files Browse the repository at this point in the history
Release/1.1.0
  • Loading branch information
klempine authored Aug 26, 2020
2 parents b74a771 + 1c92b9b commit 6ade308
Show file tree
Hide file tree
Showing 102 changed files with 3,498 additions and 796 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

## [1.1.0] - 2020-08-25
### Added
- Support for authorization code generation for GDPR API related calls (profile download and deletion) [#108](https://github.com/City-of-Helsinki/open-city-profile-ui/pull/108)
- Link to authentication method account management [#114](https://github.com/City-of-Helsinki/open-city-profile-ui/pull/114)
- Managing multiple addresses
- Managing multiple phone numbers
- Favicon

### Changed
- Better postal code validation
- Removed drop shadows
- Replaced custom select boxes with HDS Dropdown

### Fixed
- Focus indicator being partially hidden with elements used for downloading and deleting profile
- Notifications rendering on top of each other
- Order and visibility of language options
- Several issues with layout, scaling etc
- Text fixes

## [1.0.0-rc.1]
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,20 @@ Since this app uses react-scripts (Create React App) the env-files work a bit di

The following envs are used:

- REACT_APP_OIDC_AUTHORITY - this is the URL to tunnistamo
- REACT_APP_OIDC_CLIENT_ID - ID of the client that has to be configured in tunnistamo
- REACT_APP_PROFILE_AUDIENCE - name of the api-token that client uses profile-api with
- REACT_APP_PROFILE_GRAPHQL - URL to the profile graphql
- REACT_APP_OIDC_SCOPE - which scopes the app requires
- REACT_APP_SENTRY_DSN - sentry public dns-key
| Name | Description |
| --- | ------------- |
| `REACT_APP_HELSINKI_ACCOUNT_AMR` | Authentication method reference for Helsinki account. </br> **default:** `helusername` |
| `REACT_APP_IPD_MANAGEMENT_URL_HELSINKI_ACCOUNT` | Account management url for Helsinki account. </br> **default:** `https://salasana.hel.ninja/auth/realms/helsinki-salasana/account` |
| `REACT_APP_IPD_MANAGEMENT_URL_GITHUB` | Account management url for GitHub. </br> **default:** `https://github.com/settings/profile` |
| `REACT_APP_IPD_MANAGEMENT_URL_GOOGLE` | Account management url for Google. </br> **default:** `https://myaccount.google.com` |
| `REACT_APP_IPD_MANAGEMENT_URL_FACEBOOK` | Account management url for Facebook. </br> **default:** `http://facebook.com/settings` |
| `REACT_APP_IPD_MANAGEMENT_URL_YLE` | Account management url for Yle. </br> **default:** `https://tunnus.yle.fi/#omat-tiedot` |
| `REACT_APP_OIDC_AUTHORITY` | This is the URL to tunnistamo. |
| `REACT_APP_OIDC_CLIENT_ID` | ID of the client that has to be configured in tunnistamo. |
| `REACT_APP_OIDC_SCOPE` | Which scopes the app requires. |
| `REACT_APP_PROFILE_AUDIENCE` | Name of the api-token that client uses profile-api with. |
| `REACT_APP_PROFILE_GRAPHQL` | URL to the profile graphql. |
| `REACT_APP_SENTRY_DSN` | Sentry public dns-key. |


## Setting up local development environment with Docker
Expand Down Expand Up @@ -142,6 +150,8 @@ The graphql-backend for development is located at https://profiili-api.test.kuva

## Learn More

To learn more about specific choices in this repository, you can browse the [docs](/docs).

You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

To learn React, check out the [React documentation](https://reactjs.org/).
80 changes: 80 additions & 0 deletions docs/gdpr-api-authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# GDPR API compatibility

The GDPR API requires the user to allow actions on their data. Let's take downloading profile data as an example.

1) User clicks the download button
2) User is redirected to Tunnistamo which, if necessary, renders an UI the user can use to allow a set of permissions
3) User is redirected back to Helsinki profile UI and the download action is completed

In essence, we need to request the authorization code in the UI, because the user flow may contain a step that requires user input. Once this code is generated, it must be provided within the download profile query and delete profile mutation when these requests are sent to the profile backend. The backend can then use this code to make requests to all the other services for data or deletion.

## Technical explanation

This flow introduces one difficult step--the exit and re-entry into the profile UI application. This makes the download and deletion code flows more difficult to handle. In comparison, these actions were previously completed with callbacks and promises--which make use of the fact that the "same SPA session" is retained throughout the user action. After we transition into fetching the authorization code, this assumption no longer holds, but instead the application is "hard refreshed" at least once.

Within this application, this behaviour has been managed with the help of `GdprAuthorizationCodeManager`, `useActionResumer` and `useAuthorizationCode`.

### `GdprAuthorizationCodeManager`

This class is responsible for compliance with the `OpenID` protocol. It's responsible for handling the authorization flow. In this capacity it:
* Stores the application state so that it can be reused when authorization is complete
* Creates the authorization url
* Navigates to authorization url
* Interjects the authorization callback
* Saves code for use
* Reloads application state
* Deletes code and application state from store when it is no longer needed

### `useActionResumer`

This hook is an abstraction which seeks to bridge the "gap" that forms when the user is redirected to Tunnistamo and then finally back into our application. It allows other code within the application to complete actions that span a page refresh while being relatively agnostic about the method by which the application knows what action to resume when the redirection is done.

**Parameters**

| Name | Description |
| ------------- | ------------- |
| **`deferredAction`** | Name of action that get _deferred_ until the redirect back into the UI. This is used as an ID which tells `useActionResumer` whether it should run the `callback` parameter. |
| **`onActionInitialization`** | `useActionResumer` begins the action flow by calling this function. |
| **`callback`** | `useActionResumer`** calls this function when the action is resumed. |

**Humanized explanation**
`useActionResumer` provides its user with the `startAction` function. This function can be used to invoke an action that persists over page reloads. `useActionResumer` listens with a `useEffect` in order to notice when it should complete an action. When it determines that it's a suitable time, it invokes `callback`.

**Note:**
Currently `useActionResumer` relies on a search parameter to know whether it should invoke a `callback`. The `useEffect` that it uses to determine when an action should be completed is not hooked up to listen to changes in location. This way `useActionResumer` won't work unless the search parameter invoking it is present already when the component calling it is mounted. If the search parameter becomes available after component mount, the callback won't be invoked. Again from another perspective: `useActionResumer` uses the global `location` object to determine whether a search parameter is present. This means that it won't react to location changes completed through react-router for instance.

Using the global location is a sort of anti-pattern which would make it more risky to transition this application into a server rendered application for instance.

### `useAuthorizationCode`
Combines `GdprAuthorizationCodeManager` and `useActionResumer` into a single API that's easier to consume. Code that needs access to an authorization code can hook up to one with a call like this:

```
const [
startFetchingAuthorizationCode,
isAuthorizing
] = useAuthorizationCode(
'useDownloadProfile',
handleAuthorizationCodeCallback
);
```

## Technical flow

Here I've explained how the application should act in more technical detail. Developers can make use of this explanation to get a better sense of how the features relying on `authorization code` should work. I'll take the download flow as an example, but the delete flow is mostly the same.

1) User logs in
2) User expands panel for downloading user profile
3) User clicks download button
1) Download button is disabled and its label is changed
1) Current url and the download action are saved into local storage under `kuvaGdprAuthManager` prefix that's tailed by a random UUID
1) Tunnistamo authorize URL is built
1) User is redirected to Tunnistamo
6) User allows access to personal information in Tunnistamo
7) User is redirected back into the application into address `<origin>/gdpr-callback`
1) It calls `GdprAuthorizationCodeManager.authorizationTokenFetchCallback`
1) Token is saved into localStorage ready for consumption.
1) Previous app state is restored. User is redirected to the page the invoked download on and a special search parameter is added to tell `useActionResumer` instances that the one with this id should fire its callback.
1) Previous app state is cleared
8) User lands back on the profile index page based on the redirect.
1) The code is consumed--it's requested from `GdprAuthorizationCodeManager` which then clears it from its memory (localStorage).
1) The code is used to call `downloadProfile`
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "open-city-profile-ui",
"version": "1.0.0",
"version": "1.1.0",
"license": "MIT",
"private": true,
"dependencies": {
Expand All @@ -21,14 +21,15 @@
"@types/react-modal": "^3.10.1",
"@types/react-redux": "^7.1.5",
"@types/react-router-dom": "^5.1.0",
"@types/uuid": "^8.0.0",
"@types/validator": "^13.0.0",
"@types/yup": "^0.26.24",
"apollo-boost": "^0.4.4",
"classnames": "^2.2.6",
"date-fns": "^2.9.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"enzyme-to-json": "^3.5.0",
"file-saver": "^2.0.2",
"formik": "^2.0.4",
"graphql": "^14.5.8",
Expand All @@ -39,7 +40,7 @@
"i18n-iso-countries": "^5.3.0",
"i18next": "^17.3.0",
"i18next-browser-languagedetector": "^4.0.1",
"lodash": "^4.17.15",
"lodash": "^4.17.19",
"oidc-client": "^1.9.1",
"react": "^16.11.0",
"react-dom": "^16.11.0",
Expand All @@ -53,6 +54,7 @@
"redux-oidc": "^3.1.5",
"redux-starter-kit": "^1.0.0",
"typescript": "^3.7.3",
"uuid": "^8.1.0",
"validator": "^13.0.0",
"yup": "^0.27.0"
},
Expand Down
Binary file added public/favicon.ico
Binary file not shown.
3 changes: 2 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Rekisteröitymällä pääset käyttämään uusia palveluita yhdellä helpolla kirjautimisella. Yksi yhtenäinen tunnus tekee kaupunkilaisen arjesta helpompaa."
content="Rekisteröitymällä Helsingin kaupungin uusiin palveluihin, sinulle luodaan Helsinki-profiili, jonka kautta näet vaivattomasti tietosi, joita sinusta keräämme sekä palvelut joihin olet tunnistautunut."
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Profiili</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="toast-root"></div>
</body>
</html>
65 changes: 36 additions & 29 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ import AccessibilityStatement from './accessibilityStatement/AccessibilityStatem
import { MAIN_CONTENT_ID } from './common/constants';
import AccessibilityShortcuts from './common/accessibilityShortcuts/AccessibilityShortcuts';
import AppMeta from './AppMeta';
import authenticate from './auth/authenticate';
import logout from './auth/logout';
import useAuthenticate from './auth/useAuthenticate';
import authConstants from './auth/constants/authConstants';
import GdprAuthorizationCodeManagerCallback from './gdprApi/GdprAuthorizationCodeManagerCallback';
import ToastProvider from './toast/ToastProvider';

countries.registerLocale(fi);
countries.registerLocale(en);
Expand Down Expand Up @@ -57,6 +58,7 @@ type Props = {};

function App(props: Props) {
const location = useLocation();
const [authenticate, logout] = useAuthenticate();

if (location.pathname === '/loginsso') {
authenticate();
Expand All @@ -82,33 +84,38 @@ function App(props: Props) {
<ReduxProvider store={store}>
<OidcProvider store={store} userManager={userManager}>
<ApolloProvider client={graphqlClient}>
<MatomoProvider value={instance}>
<AppMeta />
{/* This should be the first focusable element */}
<AccessibilityShortcuts mainContentId={MAIN_CONTENT_ID} />
<Switch>
<Route path="/callback">
<OidcCallback />
</Route>
<Route path="/login">
<Login />
</Route>
<Route
path={['/', '/connected-services', '/subscriptions']}
exact
>
<Profile />
</Route>
<Route path="/accessibility">
<AccessibilityStatement />
</Route>
<Route path="/profile-deleted" exact>
<ProfileDeleted />
</Route>
<Route path="/loginsso" exact />
<Route path="*">404 - not found</Route>
</Switch>
</MatomoProvider>
<ToastProvider>
<MatomoProvider value={instance}>
<AppMeta />
{/* This should be the first focusable element */}
<AccessibilityShortcuts mainContentId={MAIN_CONTENT_ID} />
<Switch>
<Route path="/callback">
<OidcCallback />
</Route>
<Route path="/gdpr-callback">
<GdprAuthorizationCodeManagerCallback />
</Route>
<Route path="/login">
<Login />
</Route>
<Route
path={['/', '/connected-services', '/subscriptions']}
exact
>
<Profile />
</Route>
<Route path="/accessibility">
<AccessibilityStatement />
</Route>
<Route path="/profile-deleted" exact>
<ProfileDeleted />
</Route>
<Route path="/loginsso" exact />
<Route path="*">404 - not found</Route>
</Switch>
</MatomoProvider>
</ToastProvider>
</ApolloProvider>
</OidcProvider>
</ReduxProvider>
Expand Down
4 changes: 2 additions & 2 deletions src/accessibilityStatement/AccessibilityStatementEn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ function AccessibilityStatementEn() {
<Fragment>
<h1>Accessibility Statement</h1>
<p>
This accessibility statement applies to the website Youth membership
registration. The site address is https://hel.fi/profiili
This accessibility statement applies to the Helsinki Profile website.
The site address is https://hel.fi/profiili
</p>
<h2>Statutory provisions applicable to the website</h2>
<p>
Expand Down
3 changes: 1 addition & 2 deletions src/accessibilityStatement/AccessibilityStatementSv.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ function AccessibilityStatementSv() {
<h1>Tillgänglighetsutlåtande</h1>
<p>
Detta tillgänglighetsutlåtande gäller Helsingfors stads webbplats
Ungdomstjänsternas medlem ansökan . Webbplatsens adress är
https://hel.fi/profiili
Helsingfors-profil. Webbplatsens adress är https://hel.fi/profiili
</p>

<h2>Lagbestämmelser som gäller webbplatsen</h2>
Expand Down
14 changes: 0 additions & 14 deletions src/auth/authenticate.ts

This file was deleted.

37 changes: 26 additions & 11 deletions src/auth/components/login/Login.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,46 @@
width: 95%;
text-align: center;
color: var(--color-white);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}

.logo {
width: 170px;
height: 80px;
background-color: var(--color-white);
}

.logo g {
fill: var(--color-white);
button.button {
margin-top: 50px;
background-color: var(--color-white);
width: 100%;
}

.button {
margin-top: 50px;
button.button:hover {
background-color: var(--color-background-button-secondary-hover);
}

.content h1 {
font-size: var(--fontsize-h-2);
}

.content h2 {
line-height: var(--lineheight-l);
font-size: var(--fontsize-h-5);
}

.content button {
min-width: auto;
@media(min-width: 450px) {
button.button {
width: 230px;
}

.content h1 {
font-size: var(--fontsize-h-1);
}

}

@media (min-width: 600px) {
Expand All @@ -41,8 +60,4 @@
}
}

@media (min-width: 230px) {
.content button {
min-width: 230px;
}
}

Loading

0 comments on commit 6ade308

Please sign in to comment.