Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expire maintainer invites and scheduled tasks for the database #1221

Merged
merged 3 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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