Skip to content

Commit

Permalink
Merge pull request #1221 from research-software-directory/260-maintai…
Browse files Browse the repository at this point in the history
…ner-invitation-improvements

Expire maintainer invites and scheduled tasks for the database
  • Loading branch information
ewan-escience authored Jun 10, 2024
2 parents 0e7dc4d + 56a6511 commit e74c1d2
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 22 deletions.
19 changes: 11 additions & 8 deletions database/015-create-maintainer-tables.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-- SPDX-FileCopyrightText: 2021 - 2022 Ewan Cahen (Netherlands eScience Center) <[email protected]>
-- SPDX-FileCopyrightText: 2021 - 2022 Netherlands eScience Center
-- SPDX-FileCopyrightText: 2021 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
-- SPDX-FileCopyrightText: 2021 - 2024 Netherlands eScience Center
-- SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
-- SPDX-FileCopyrightText: 2022 dv4all
--
Expand Down Expand Up @@ -31,7 +31,8 @@ CREATE TABLE invite_maintainer_for_project (
created_by UUID REFERENCES account (id),
claimed_by UUID REFERENCES account (id),
claimed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT LOCALTIMESTAMP
created_at TIMESTAMPTZ NOT NULL DEFAULT LOCALTIMESTAMP,
expires_at TIMESTAMP NOT NULL GENERATED ALWAYS AS (created_at AT TIME ZONE 'UTC' + INTERVAL '31 days') STORED
);

CREATE FUNCTION sanitise_insert_invite_maintainer_for_project() RETURNS TRIGGER LANGUAGE plpgsql AS
Expand Down Expand Up @@ -81,7 +82,7 @@ BEGIN
RAISE EXCEPTION USING MESSAGE = 'Invitation with id ' || invitation || ' does not exist';
END IF;

IF invitation_row.claimed_by IS NOT NULL OR invitation_row.claimed_at IS NOT NULL THEN
IF invitation_row.claimed_by IS NOT NULL OR invitation_row.claimed_at IS NOT NULL OR invitation_row.expires_at < CURRENT_TIMESTAMP THEN
RAISE EXCEPTION USING MESSAGE = 'Invitation with id ' || invitation || ' is expired';
END IF;

Expand All @@ -105,7 +106,8 @@ CREATE TABLE invite_maintainer_for_software (
created_by UUID REFERENCES account (id),
claimed_by UUID REFERENCES account (id),
claimed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT LOCALTIMESTAMP
created_at TIMESTAMPTZ NOT NULL DEFAULT LOCALTIMESTAMP,
expires_at TIMESTAMP NOT NULL GENERATED ALWAYS AS (created_at AT TIME ZONE 'UTC' + INTERVAL '31 days') STORED
);

CREATE FUNCTION sanitise_insert_invite_maintainer_for_software() RETURNS TRIGGER LANGUAGE plpgsql AS
Expand Down Expand Up @@ -158,7 +160,7 @@ BEGIN
RAISE EXCEPTION USING MESSAGE = 'Invitation with id ' || invitation || ' does not exist';
END IF;

IF invitation_row.claimed_by IS NOT NULL OR invitation_row.claimed_at IS NOT NULL THEN
IF invitation_row.claimed_by IS NOT NULL OR invitation_row.claimed_at IS NOT NULL OR invitation_row.expires_at < CURRENT_TIMESTAMP THEN
RAISE EXCEPTION USING MESSAGE = 'Invitation with id ' || invitation || ' is expired';
END IF;

Expand All @@ -182,7 +184,8 @@ CREATE TABLE invite_maintainer_for_organisation (
created_by UUID REFERENCES account (id),
claimed_by UUID REFERENCES account (id),
claimed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT LOCALTIMESTAMP
created_at TIMESTAMPTZ NOT NULL DEFAULT LOCALTIMESTAMP,
expires_at TIMESTAMP NOT NULL GENERATED ALWAYS AS (created_at AT TIME ZONE 'UTC' + INTERVAL '31 days') STORED
);

CREATE FUNCTION sanitise_insert_invite_maintainer_for_organisation() RETURNS TRIGGER LANGUAGE plpgsql AS
Expand Down Expand Up @@ -235,7 +238,7 @@ BEGIN
RAISE EXCEPTION USING MESSAGE = 'Invitation with id ' || invitation || ' does not exist';
END IF;

IF invitation_row.claimed_by IS NOT NULL OR invitation_row.claimed_at IS NOT NULL THEN
IF invitation_row.claimed_by IS NOT NULL OR invitation_row.claimed_at IS NOT NULL OR invitation_row.expires_at < CURRENT_TIMESTAMP THEN
RAISE EXCEPTION USING MESSAGE = 'Invitation with id ' || invitation || ' is expired';
END IF;

Expand Down
7 changes: 7 additions & 0 deletions documentation/docs/01-users/05-adding-software.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ The related projects sections can be used to link related project pages in the R

Here, you can see all the people who can maintain this software page. You can also create invitation links to send to people you want to give maintainer access and see and delete all unused invitations.

:::info

- Each invitation link can be used only once.
- Each invitation expires after 31 day and can be removed before the expiry date as well.

:::

## Background services

Here you can find the information about the background services that RSD offers and their last status.
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/01-users/07-adding-projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ Here, you can see all the people who can maintain this project page. You can als
:::info

- Each invitation link can be used only once.
- The link does not have expiration date, but you can remove unused invitation manually.
- Each invitation expires after 31 day and can be removed before the expiry date as well.

:::

Expand Down
11 changes: 9 additions & 2 deletions documentation/docs/01-users/09-organisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,17 @@ Under __"Settings - General settings"__ you can edit:
As a maintainer, you can invite or remove other maintainers from your organisation.

:::warning
The __primary maintainer__ of an organisation is defined by rsd administrators. If you want to change the primary maintainer, contact us via [[email protected]](mailto:[email protected]).
The __primary maintainer__ of an organisation is set by RSD administrators. If you want to change the primary maintainer, contact us via [[email protected]](mailto:[email protected]).
:::

To invite new maintainers, click on __"Generate invite link"__. A link will be generated. You can either copy this link or click on "Email this invite" to open your mail program with a preformulated email.
To invite new maintainers, click on __"Generate invite link"__. A link will be generated. You can either copy this link or click on "Email this invite" to open your mail program with a pre-formulated email.

:::info

- Each invitation link can be used only once.
- Each invitation expires after 31 day and can be removed before the expiry date as well.

:::

![animation](img/organisation-maintainer-invite.gif)

Expand Down
4 changes: 2 additions & 2 deletions documentation/docs/01-users/09-organisation.md.license
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center
SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) <[email protected]>
SPDX-FileCopyrightText: 2023 Netherlands eScience Center
SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) <[email protected]>
SPDX-FileCopyrightText: 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences

Expand Down
16 changes: 16 additions & 0 deletions documentation/docs/03-rsd-instance/04-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,19 @@ You can now run arbitrary SQL queries as root user.
We [publish database migration script during the release](https://github.com/research-software-directory/RSD-as-a-service/releases). The migration script can be used to upgrade the database structure from the previous version to released version. We use the published database migration script to update out production RSD instance.

All [migration scripts are stored in our production repository](https://github.com/research-software-directory/RSD-production/tree/main/database-migration).

## Scheduling repeated tasks

We recommend to use [`cron`](https://en.wikipedia.org/wiki/Cron) to schedule repeated tasks, like [Routine Database Maintenance Tasks](https://www.postgresql.org/docs/current/maintenance.html). You can use [crontab guru](https://crontab.guru/) to assist with making and understanding crontab entries.

As an example, to clean up maintainer invites older than a year, you could add the following entries to your crontab (making sure to replace values where applicable, e.g. the file location):

```
30 1 * * * docker-compose --file /home/ubuntu/docker-compose.yml exec -T database psql --dbname=rsd-db --username=rsd --command="DELETE FROM invite_maintainer_for_software WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '1 year';"
30 2 * * * docker-compose --file /home/ubuntu/docker-compose.yml exec -T database psql --dbname=rsd-db --username=rsd --command="DELETE FROM invite_maintainer_for_project WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '1 year';"
30 3 * * * docker-compose --file /home/ubuntu/docker-compose.yml exec -T database psql --dbname=rsd-db --username=rsd --command="DELETE FROM invite_maintainer_for_organisation WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '1 year';"
```

You can append `>> /home/ubuntu/cron.log 2>&1` to a line to write all output and error messages to a text file.

As an alternative to `cron`, you could use [pg_cron](https://github.com/citusdata/pg_cron) instead. This will require some more work to install it, set it up and keeping it up to date.
4 changes: 2 additions & 2 deletions documentation/docs/03-rsd-instance/04-database.md.license
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center
SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) <[email protected]>
SPDX-FileCopyrightText: 2023 Netherlands eScience Center

SPDX-License-Identifier: CC-BY-4.0
19 changes: 17 additions & 2 deletions frontend/components/layout/InvitationList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 dv4all
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
//
Expand Down Expand Up @@ -42,7 +42,18 @@ export default function InvitationList({invitations, token, onDeleteCallback}: {
}
}

function getExpiredText(daysValid: number): string {
if (daysValid <= 0) {
return 'this invitation is expired'
} else if (daysValid === 1) {
return 'expires in less than a day'
} else {
return `expires in ${daysValid} days`
}
}

if(invitations.length === 0) return null
const now = new Date()

return (
<>
Expand All @@ -53,9 +64,13 @@ export default function InvitationList({invitations, token, onDeleteCallback}: {
<List>
{invitations.map(inv => {
const currentLink = `${location.origin}/invite/${inv.type}/${inv.id}`
const expiresAt = new Date(inv.expires_at)
const daysValid = Math.ceil((expiresAt.valueOf() - now.valueOf()) / (1000 * 60 * 60 * 24))
let expiredText: string
expiredText = getExpiredText(daysValid);
return (
<ListItem key={inv.id} disableGutters>
<ListItemText primary={'Created on ' + new Date(inv.created_at).toDateString()} secondary={currentLink}/>
<ListItemText primary={'Created on ' + new Date(inv.created_at).toDateString() + ', ' + expiredText} secondary={currentLink}/>
<IconButton onClick={() => toClipboard(currentLink)}><CopyIcon/></IconButton>
<IconButton onClick={() => deleteMaintainerLink(inv)}><DeleteIcon/></IconButton>
</ListItem>
Expand Down
5 changes: 3 additions & 2 deletions frontend/types/Invitation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

export type Invitation = {
id: string,
created_at: string,
expires_at: string,
type: 'software' | 'project' | 'organisation'
}
6 changes: 3 additions & 3 deletions frontend/utils/getUnusedInvitations.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

import {Invitation} from '~/types/Invitation'
import {createJsonHeaders} from './fetchHelpers'

export async function getUnusedInvitations(type: 'software' | 'project' | 'organisation', id: string, token?: string) {
const resp = await fetch(`/api/v1/invite_maintainer_for_${type}?select=id,created_at&order=created_at&${type}=eq.${id}&claimed_by=is.null&claimed_at=is.null`, {
const resp = await fetch(`/api/v1/invite_maintainer_for_${type}?select=id,created_at,expires_at&order=created_at&${type}=eq.${id}&claimed_by=is.null&claimed_at=is.null`, {
headers: createJsonHeaders(token)
})
const respJson: Invitation[] = await resp.json()
Expand Down

0 comments on commit e74c1d2

Please sign in to comment.