diff --git a/database/015-create-maintainer-tables.sql b/database/015-create-maintainer-tables.sql index d79c0b4bd..da1964410 100644 --- a/database/015-create-maintainer-tables.sql +++ b/database/015-create-maintainer-tables.sql @@ -1,5 +1,5 @@ --- SPDX-FileCopyrightText: 2021 - 2022 Ewan Cahen (Netherlands eScience Center) --- SPDX-FileCopyrightText: 2021 - 2022 Netherlands eScience Center +-- SPDX-FileCopyrightText: 2021 - 2024 Ewan Cahen (Netherlands eScience Center) +-- SPDX-FileCopyrightText: 2021 - 2024 Netherlands eScience Center -- SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -- SPDX-FileCopyrightText: 2022 dv4all -- @@ -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 @@ -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; @@ -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 @@ -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; @@ -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 @@ -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; diff --git a/documentation/docs/01-users/05-adding-software.md b/documentation/docs/01-users/05-adding-software.md index f7bcaaf7c..b45c21e13 100644 --- a/documentation/docs/01-users/05-adding-software.md +++ b/documentation/docs/01-users/05-adding-software.md @@ -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. diff --git a/documentation/docs/01-users/07-adding-projects.md b/documentation/docs/01-users/07-adding-projects.md index 36f34fe29..bf0eff51a 100644 --- a/documentation/docs/01-users/07-adding-projects.md +++ b/documentation/docs/01-users/07-adding-projects.md @@ -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. ::: diff --git a/documentation/docs/01-users/09-organisation.md b/documentation/docs/01-users/09-organisation.md index 32265e512..2163fe111 100644 --- a/documentation/docs/01-users/09-organisation.md +++ b/documentation/docs/01-users/09-organisation.md @@ -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 [rsd@esciencecenter.nl](mailto:rsd@esciencecenter.nl). +The __primary maintainer__ of an organisation is set by RSD administrators. If you want to change the primary maintainer, contact us via [rsd@esciencecenter.nl](mailto:rsd@esciencecenter.nl). ::: -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) diff --git a/documentation/docs/01-users/09-organisation.md.license b/documentation/docs/01-users/09-organisation.md.license index 178bcce1a..75111bbfb 100644 --- a/documentation/docs/01-users/09-organisation.md.license +++ b/documentation/docs/01-users/09-organisation.md.license @@ -1,6 +1,6 @@ +SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) +SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) -SPDX-FileCopyrightText: 2023 Netherlands eScience Center SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) SPDX-FileCopyrightText: 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences diff --git a/documentation/docs/03-rsd-instance/04-database.md b/documentation/docs/03-rsd-instance/04-database.md index b687ec4d9..3023b2bb0 100644 --- a/documentation/docs/03-rsd-instance/04-database.md +++ b/documentation/docs/03-rsd-instance/04-database.md @@ -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. diff --git a/documentation/docs/03-rsd-instance/04-database.md.license b/documentation/docs/03-rsd-instance/04-database.md.license index 715dce5aa..d42e2bb9b 100644 --- a/documentation/docs/03-rsd-instance/04-database.md.license +++ b/documentation/docs/03-rsd-instance/04-database.md.license @@ -1,5 +1,5 @@ +SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) +SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) -SPDX-FileCopyrightText: 2023 Netherlands eScience Center SPDX-License-Identifier: CC-BY-4.0 diff --git a/frontend/components/layout/InvitationList.tsx b/frontend/components/layout/InvitationList.tsx index 4686a3d9f..2ace4c899 100644 --- a/frontend/components/layout/InvitationList.tsx +++ b/frontend/components/layout/InvitationList.tsx @@ -1,6 +1,6 @@ +// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 dv4all // SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) // @@ -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 ( <> @@ -53,9 +64,13 @@ export default function InvitationList({invitations, token, onDeleteCallback}: { {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 ( - + toClipboard(currentLink)}> deleteMaintainerLink(inv)}> diff --git a/frontend/types/Invitation.ts b/frontend/types/Invitation.ts index 87e305845..36a29edf9 100644 --- a/frontend/types/Invitation.ts +++ b/frontend/types/Invitation.ts @@ -1,10 +1,11 @@ -// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) +// 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' } diff --git a/frontend/utils/getUnusedInvitations.ts b/frontend/utils/getUnusedInvitations.ts index 9c3a63f09..c3c347c16 100644 --- a/frontend/utils/getUnusedInvitations.ts +++ b/frontend/utils/getUnusedInvitations.ts @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -7,7 +7,7 @@ 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()