Skip to content

Commit

Permalink
feat(Notifications): new notifications FE plugin, API and backend (#933)
Browse files Browse the repository at this point in the history
* draft

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* feat(parodos-notifications): parodos plugin for notifications

* chore(parodos-notifications): add plugin metadata

* Add Parodos to the left-side menu

* Add ParodosNotificationsTable

* Remove page header

* Rename plugin to @backstage/plugin-parodos-notifications

* Fix types in myplugin-backend

* Hotfix Knex type

* chore(parodos-notifications): Add tsconfig and turbo to the backend plugin

* Fix typo in constants

* Add DB mock for backend tests

* Read notifications from backend

* Add rxjs dependency

* First Notifaction API with Observable

* Rename backend plugin to notifications-backend

* Reimplement the Notifications to use tabs

* Fix endless "loading" state for an empty notifications list

* Rename front-end plugin to notifications-frontend

* Add coverage and passWithNoTests to notifications-frontend

* filter paging users

first version of our implementation
it shows:
- plugin independent of DB
- text filtering
- paging
- users and groups validation

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* fix flpath-659
catch DB errors in router handlers
https://issues.redhat.com/browse/FLPATH-659

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* Add patternfly/react-core among dependencies

* Remove rxjs dependency

* Add new fields to the UI

- UUID
- Created
- Title
- Origin
- Message
- Topic
- Links

* Rename System notifications tab

* Use Item instead of Group for the left-side menu

* Mark "unread" notifications in the left-side menu

* Update README to match recent endpoints

* Add default sorting

* Add pagination

* Remove TODO

* FLPATH-661: Add notification actions

Closes FLPATH-661
Closes FLPATH-662

* Make the actions.title mandatory

* BE- implement posting to users and groups
FLPATH-665
https://issues.redhat.com/browse/FLPATH-665

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* FLPATH-670: Add filtering by text

* Remove unused code - FE notificationsService.ts

* Add @material-table/core among dependencies

* FLPATH-671: Add Created After filter

* FLPATH-671: Add Created After selection

* FLPATH-668: Add sorting

* Add stub for passing users

To unblock due to conflicts now - will be done properly in a follow-up.

* Move sorting params validation to handler

* Polling in NotificationsSidebarItem can be configured

* FLPATH-675: Show system-wide notifications in the Updates tab

* FLPATH-675: Show system-wide alerts

* update README

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* Simplify BE cehck for orderBy

* Avoid using Patternfly in the Notifications

* Add react-router-dom to notifications-frontend

* Implement creating of notifications

* Add a page to create notifications

Implemented as a tab on the NotificationsPage.

The new tab is not displayed by default, only when directly
navigated by the URL ([BACKSTAGE]/notifications/send).

Meant for demonstration and debug purposes since the notifications
are expected to be send by 3rd party FE/BE plugins or external systems.

* BE - implement mark as read
flpath-666
https://issues.redhat.com/browse/FLPATH-666

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* FLPATH-664: Implement Mark as Read on FE

* FLPATH-663: Pass logged-in user name to the backend

* Add defaut/guest user to the Catalog

Within example data.

* Update README for authentication section

* FLPATH-667: Add notifications-common library

* Add more descriptions

* Fix package.json after rebase

* Sync dependencies with main

* add defaults for GET requests
FLPATH-669
https://issues.redhat.com/browse/FLPATH-669

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* add indexes to db columns
flpath727
https://issues.redhat.com/browse/FLPATH-727

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* FLPATH-738: render actions horizontally

* FLPATH-738: use a dropdown for the Unread filter

* FLPATH-738: Change "mark as read" icons

* openapi yaml for rest api
FLPATH-748
https://issues.redhat.com/browse/FLPATH-748

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* create DB client in BE that supports all Knex configuration
flpath-660
https://issues.redhat.com/browse/FLPATH-660

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* add cascade to foreign key in actions table

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* Add debouncing to the search filter

* add date-time format to time fields

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* Make 'count' required in response

* Fix required action fields in the OpenApi spec

* Add openapi client generator to the notifications-frontend

* Regenerate openapi client in the frontend

* Remove notifications-common from FE dependencies

We do not need it anymore since we have the generated types from
OpenApi.

* Update frontend to use generated openapi calls

* Add ts-nocheck to generated OpenApi code

* Regenerate openapi

* remove notifications-common
FLPATH-755
https://issues.redhat.com/browse/FLPATH-755

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* Notifications-frontend install instructions and deregister

The plugin is removed from the Backstage

* Notifications-backend install instructions and deregister

The plugin is removed from the Backstage

* create router in BE based on openapi spec
FLPATH756
https://issues.redhat.com/browse/FLPATH-756

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* fix getNotificationsCount to return type number

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* support sqllite and other DBs
FLPATH-811
https://issues.redhat.com/browse/FLPATH-811

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* FLPATH-809: get the user via Identity API

With this patch, the username is not passed from FE to BE anymore but
retrieved from via Identity API as the logged-in user.

* chore(notifications-backend): add permissions related deps

* chore: update README for authentication

* chore: make service-to-service checks optional

* chore: reduce notifications "read" permissions count

* chore: separate getLoggedInUser and checkPermission funcs

* Introduce auth.ts and shared secret for external callers

* chore: update README for SQLite

* add unit test to BE
FLPATH-798
https://issues.redhat.com/browse/FLPATH-798

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>

* chore: docs linter

* chore: use default/guest for external calls

Authentication of external systems is not stable anyway, so using the
default/guest for them will simplify testing.

* chore: fix handling of promises in tests

* chore: remove forgotten router-test-ts

* chore: update yarn.lock after rebase

* chore: add --coverage to tests to pass the CI

* chore: hotfix for types mismatch for KNex MockClient

* chore: unify dep versions in notifications-backend

---------

Signed-off-by: Yaron Dayagi <ydayagi@redhat.com>
Co-authored-by: Yaron Dayagi <ydayagi@redhat.com>
  • Loading branch information
mareklibra and ydayagi authored Jan 11, 2024
1 parent ceb16a9 commit 4d4cb78
Show file tree
Hide file tree
Showing 75 changed files with 7,830 additions and 141 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ yarn.lock @janus-idp/maintainers-plugins
/plugins/tekton/ @janus-idp/maintainers-plugins @debsmita1 @divyanshiGupta
/plugins/rbac-backend/ @janus-idp/maintainers-plugins @gorkem @AndrienkoAleksandr @PatAKnight
/plugins/rbac-common/ @janus-idp/maintainers-plugins @gorkem @AndrienkoAleksandr @PatAKnight

/plugins/notifications-frontend @janus-idp/maintainers-plugins @mareklibra
1 change: 0 additions & 1 deletion app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ backend:
# This is for local development only, it is not recommended to use this in production
# The production database configuration is stored in app-config.production.yaml
database:
client: better-sqlite3
connection: ':memory:'
cache:
store: memory
Expand Down
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@backstage/plugin-catalog-import": "^0.10.1",
"@backstage/plugin-catalog-react": "^1.8.5",
"@backstage/plugin-github-actions": "^0.6.6",
"@backstage/plugin-notifications-frontend": "0.1.0",
"@backstage/plugin-org": "^0.6.15",
"@backstage/plugin-permission-react": "^0.4.16",
"@backstage/plugin-scaffolder": "^1.15.1",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/components/Root/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
</SidebarScrollWrapper>
</SidebarGroup>
<SidebarDivider />
<SidebarSpace />
<SidebarDivider />
<SidebarGroup
Expand Down
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@backstage/plugin-auth-backend": "^0.19.3",
"@backstage/plugin-auth-node": "^0.4.0",
"@backstage/plugin-catalog-backend": "^1.14.0",
"@backstage/plugin-notifications-backend": "^0.1.0",
"@backstage/plugin-permission-backend": "^0.5.29",
"@backstage/plugin-permission-common": "^0.7.9",
"@backstage/plugin-permission-node": "^0.7.17",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"tsc": "tsc",
"test": "backstage-cli package test",
"test": "backstage-cli package test --passWithNoTests --coverage",
"clean": "backstage-cli package clean",
"start": "nodemon --"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/matomo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"postversion": "yarn run export-dynamic",
"prepack": "backstage-cli package prepack",
"start": "backstage-cli package start",
"test": "backstage-cli package test",
"test": "backstage-cli package test --passWithNoTests --coverage",
"tsc": "tsc"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions plugins/notifications-backend/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
304 changes: 304 additions & 0 deletions plugins/notifications-backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
# Notifications

This Backstage backend plugin provides REST API endpoint for the notifications.

It's backed by a relational database, so far tested with PostgreSQL.

## Getting started

The plugin uses a relational database to persist messages, it has been tested with the SQLite and PostgreSQL.

Upon backend's plugin start, the `backstage_plugin_notifications` database and its tables are created automatically.

### Optional: PostgreSQL

**To use the Backstage's default SQLite, no specific configuration is needed.**

Following steps describe requirements for PostgreSQL:

- Install [PostgresSQL DB](https://www.postgresql.org/download/)
- Configure Postgres for tcp/ip
Open Postgres conf file for editing:

```bash
sudo vi /var/lib/pgsql/data/pg_hba.conf
```

Add this line:

```bash
host all postgres 127.0.0.1/32 password
```

- Start Postgres server:

```bash
sudo systemctl enable --now postgresql.service
```

#### Backstage's configuration for PostgreSQL

If PostgreSQL is used, additional configuration in the in the `app-config.yaml` or `app-config.local.yaml` is needed. Example:

```
database:
client: pg
connection:
host: 127.0.0.1
port: 5432
user: postgres
password: your_secret
knexConfig:
pool:
min: 3
max: 12
acquireTimeoutMillis: 60000
idleTimeoutMillis: 60000
cache:
store: memory
```

### Add NPM dependency

```
cd packages/backend
yarn add @backstage/plugin-notifications-backend
```

### Add backend-plugin

Create `packages/backend/src/plugins/notifications.ts` with following content:

```
import { CatalogClient } from '@backstage/catalog-client';
import { createRouter } from '@backstage/plugin-notifications-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogClient = new CatalogClient({ discoveryApi: env.discovery });
const dbConfig = env.config.getConfig('backend.database');
// Following is optional
const externalCallerSecret = env.config.getOptionalString(
'notifications.externalCallerSecret',
);
return await createRouter({
identity: env.identity,
logger: env.logger,
permissions: env.permissions,
tokenManager: env.tokenManager,
dbConfig,
catalogClient,
externalCallerSecret,
});
}
```

### Add to router

In the `packages/backend/src/index.ts`:

```
import notifications from './plugins/notifications';
...
{/* Existing code for reference: */}
const apiRouter = Router();
...
{/* New code: */}
const notificationsEnv = useHotMemoize(module, () =>
createEnv('notifications'),
);
apiRouter.use('/notifications', await notifications(notificationsEnv));
```

### Configure

#### Optional: Plugin's configuration

If you have issues to create valid JWT tokens by an external caller, use following option to bypass the service-to-service configuration for them:

```
notifications:
# Workaround for issues with external caller JWT token creation.
# When following config option is not provided and the request "authentication" header is missing, the request is ALLOWED by default
# When following option is present, the request must contain either a valid JWT token or that provided shared secret in the "notifications-secret" header
externalCallerSecret: your-secret-token-shared-with-external-services
```

Mind using HTTPS to help preventing leaking the shared secret.

Example of the request then:

```
curl -X POST http://localhost:7007/api/notifications/notifications -H "Content-Type: application/json" -H "notifications-secret: your-secret-token-shared-with-external-services" -d '{"title":"my-title","origin":"my-origin","message":"message one","topic":"my-topic"}'
```

Notes:

- The `externalCallerSecret` is an workaround, exclusive use of JWT tokens will probably be required in the future.
- Sharing the same shared secret with the "auth.secret" option is not recommended.

#### Authentication

Please refer https://backstage.io/docs/auth/ to set-up authentication.

The Notifications flows are based on the identity of the user.

All `targetUsers`, `targetGroups`` or signed-in users receiving notifications must have corresponding entities created in the Catalog.
Refer https://backstage.io/docs/auth/identity-resolver for details.

For the purpose of development, there is `users.yaml` listing example data created.

#### Authorization

Every service endpoint is guarded by a permission check, enabled by default.

It is up to particular deployment to provide corresponding permission policies based on https://backstage.io/docs/permissions/writing-a-policy. To register your permission policies, refer https://backstage.io/docs/permissions/getting-started#integrating-the-permission-framework-with-your-backstage-instance.

#### Service-to-service and External Calls

The notification-backend is expected to be called by FE plugins (including the notifications-frontend), other backend plugins or external services.

To configure those two flows, refer

- https://backstage.io/docs/auth/service-to-service-auth.
- https://backstage.io/docs/auth/service-to-service-auth#usage-in-external-callers

#### Catalog

The notifications require target users or groups (as receivers) to be listed in the Catalog.

As an example how to do it, add following to the config:

```
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
# *** Here is new change:
- allow: [Component, System, API, Resource, Location, User, Group]
locations:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
# *** Here is new change, referes to a file stored in the root of the Backstage:
target: ../../users.yaml
```

The example list of users is stored in the `plugins/notifications-backend/users.yaml` and can be copied to the root of the Backstage for development purposes.

## REST API

See `src/openapi.yaml` for full OpenAPI spec.

### Posting a notification

A notification without target users or groups is considered a system notification. That means it is intended for all users (listed among Updates in the UI).

Request (User message and then system message):

```bash
curl -X POST http://localhost:7007/api/notifications/notifications -H "Content-Type: application/json" -d '{"title": "My message title", "message": "I have nothing to say", "origin": "my-origin", "targetUsers": ["jdoe"], "targetGroups": ["jdoe"], "actions": [{"title": "my-title", "url": "http://foo.bar"}, {"title": "another action", "url": "https://foo.foo.bar"}]}'
```

```bash
curl -X POST http://localhost:7007/api/notifications/notifications -H "Content-Type: application/json" -d '{"title": "My message title", "message": "I have nothing to say", "origin": "my-origin", "actions": [{"title": "my-title", "url": "http://foo.bar"}, {"title": "another action", "url": "https://foo.foo.bar"}]}'
```

Optionally add `-H "Authorization: Bearer eyJh.....` with a valid JWT token if the service-to-service authorization is enabled (see above).

Response:

```json
{ "msgid": "2daac6ff-3aaa-420d-b755-d94e54248310" }
```

### Get notifications

Page number starts at '1'. Page number '0' along with page size '0' means no paging.
User parameter is mandatory because it is needed for message status and filtering (read/unread).

Query parameters:

- `pageSize`. 0 means no paging.
- `pageNumber`. first page is 1. 0 means no paging.
- `orderBy`.
- `orderByDirec`. asc/desc
- `containsText`. filter title and message containing this text (case insensitive)
- `createdAfter`. fetch notifications created after this point in time
- `messageScope`. all/user/system. fetch notifications intended for specific user or system notifications or both
- `read`. true/false (read/unread)

Request:

```bash
curl 'http://localhost:7007/api/notifications/notifications?read=false&pageNumber=0&pageSize=0'
```

Response:

```json
[
{
"id": "2daac6ff-3aaa-420d-b755-d94e54248310",
"created": "2023-10-30T13:48:34.931Z",
"isSystem": false,
"readByUser": false,
"origin": "my-origin",
"title": "My title",
"message": "I have nothing to tell",
"topic": "my-topic",
"actions": []
}
]
```

### Get count of notifications

User parameter is mandatory because it is needed for filtering (read/unread).

**Important: Logged-in user:**

The query requires a signed-in user whose entity is listed in the Catalog.
With this condition is met, the HTTP `Authorization` header contains a JWT token with the user's identity.

Optionally add `-H "Authorization: Bearer eyJh.....` with a valid JWT token to the `curl` commands bellow.

Query parameters:

- `containsText`. filter title and message containing this text (case insensitive)
- `createdAfter`. fetch notifications created after this point in time
- `messageScope`. all/user/system. fetch notifications intended for specific user or system notifications or both
- `read`. true/false (read/unread)

Request:

```bash
curl http://localhost:7007/api/notifications/notifications/count
```

Response:

```json
{ "count": "1" }
```

### Set notification as read/unread

Request:

```bash
curl -X PUT 'http://localhost:7007/api/notifications/notifications/read?messageID=48bbf896-4b7c-4b68-a446-246b6a801000&read=true'
```

Response: just HTTP status

## Building a client for the API

We supply an Open API spec YAML file: openapi.yaml.
Loading

0 comments on commit 4d4cb78

Please sign in to comment.