+ );
+ }
+}
+
+export default PrivilegeManager;
diff --git a/front-end/src/components/Domain/AccessManager/AccessManager.tsx b/front-end/src/components/Domain/AccessManager/AccessManager.tsx
new file mode 100644
index 0000000..7b423da
--- /dev/null
+++ b/front-end/src/components/Domain/AccessManager/AccessManager.tsx
@@ -0,0 +1,387 @@
+import React from 'react';
+import { Button, Form, Input, message, Modal, Progress, Select, Table, Tooltip } from 'antd';
+import Domain, { Access, UserWithAccess } from '@models/Domain';
+import UserSearch from '@components/UserSearch/UserSearch';
+import { UserAddOutlined } from '@ant-design/icons';
+import UserInfo from '@models/User';
+import { ColumnsType } from 'antd/lib/table';
+import { getSession } from 'next-auth/client';
+
+const { Option } = Select;
+
+/**
+ * The props for the {@link AccessManager} component.
+ */
+interface AccessManagerProps {
+ /** The domain who's access to manage. */
+ domain: Domain,
+ /** Callback function when the ownership is transferred to another user. */
+ onOwnershipTransferred: (newOwner: UserInfo) => void,
+}
+
+/**
+ * The state of the {@link AccessManager} component.
+ */
+interface AccessManagerState {
+ /** The user of the current session. */
+ currentUser: UserInfo,
+ /** The users which have access to the domain (except: Owner, Revoked). */
+ usersWithAccess: UserWithAccess[],
+ /** The currently selected access level for adding new users. */
+ addUserSelectedRole: Access,
+ /** Whether the transfer ownership modal is opened. */
+ transferOwnershipModalVisible: boolean,
+}
+
+/**
+ * Component for managing access to a domain.
+ */
+class AccessManager extends React.Component {
+ /** React RefObject to refer to the UserSearch element. */
+ userSearchRef: React.RefObject;
+
+ constructor(props: AccessManagerProps) {
+ super(props);
+
+ this.userSearchRef = React.createRef();
+ this.state = {
+ currentUser: null,
+ usersWithAccess: [],
+ addUserSelectedRole: Access.Read,
+ transferOwnershipModalVisible: false,
+ };
+ }
+
+ async componentDidMount() {
+ // Get the current user from the session
+ await getSession({}).then((user: any) => this.setState({ currentUser: user.user }));
+ this.getUsersWithAccess();
+ }
+
+ /**
+ * Get all users with access to the domain.
+ */
+ getUsersWithAccess = async () => {
+ const { domain } = this.props;
+ const accessLevels = ['Read', 'ReadWrite'].join('/');
+
+ const endpoint = `${process.env.NEXT_PUBLIC_FE_URL}/api/domain/access/${domain.id}/${accessLevels}`;
+ await fetch(endpoint, {
+ method: 'GET',
+ })
+ .then((response) => response.json())
+ .then((data) => this.setState({ usersWithAccess: data }));
+ };
+
+ /**
+ * Add table keys to the UserDomainAccess objects.
+ * @param userDomainAccessList The user domain access information array.
+ */
+ addKeys = (userDomainAccessList: UserWithAccess[]) => {
+ const result = [];
+ userDomainAccessList.forEach((access, index) => {
+ result.push({
+ // Add key
+ key: index.toString(),
+ // Add the access information
+ ...access,
+ });
+ });
+ return result;
+ };
+
+ /**
+ * Send the updated access level to the back-end.
+ * @param value The access level.
+ */
+ updateAccessLevel = async (userId: string, value: Access) => {
+ const { domain } = this.props;
+ const endpoint = `${process.env.NEXT_PUBLIC_FE_URL}/api/domain/access/${domain.id}`;
+ await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ userId, access: value }),
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ message.error('Failed to update the permission');
+ }
+ });
+ };
+
+ /**
+ * Revoke a user's access to the domain.
+ * @param userId The id of the user who's access is being revoked.
+ */
+ onRevoke = (userId: string) => {
+ const { domain } = this.props;
+ const endpoint = `${process.env.NEXT_PUBLIC_FE_URL}/api/domain/access/${domain.id}`;
+ fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ userId, access: Access.Revoked }),
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ message.error('Failed to revoke access');
+ }
+ this.getUsersWithAccess();
+ });
+ };
+
+ /**
+ * Callback function for when the access level select for adding users changes.
+ * @param value The new access level.
+ */
+ onAddUserRoleChange = (value: Access) => {
+ this.setState({ addUserSelectedRole: value });
+ };
+
+ /**
+ * When a user is selected to be added, send a request to add the user.
+ * @param user The user who is being added.
+ */
+ onAddUser = async (user: UserInfo) => {
+ const { addUserSelectedRole } = this.state;
+ await this.updateAccessLevel(user.userId, addUserSelectedRole);
+ this.getUsersWithAccess();
+ this.userSearchRef.current.setValue(null);
+ };
+
+ /**
+ * Validate that the user that is added to the permissions table is not the owner of the domain.
+ * @param email The email address that is filled in.
+ * @returns True if the user may be added, false if the user may not be added.
+ */
+ validateAddUser = (email: string): boolean => {
+ const { currentUser } = this.state;
+
+ /*
+ * The current user is the owner, the owner may not downgrade his/her own permissions this way.
+ * Check if the current user is about to downgrade his/her own permissions.
+ * If so, validation fails.
+ */
+ if (email === currentUser.email) {
+ message.warning('You cannot add yourself to the permissions list when you are the owner of the domain');
+ return false;
+ }
+ return true;
+ };
+
+ /**
+ * Callback for the transfer ownership modal, when the domain ownership should be transferred.
+ * @param newOwner The new owner of the domain.
+ */
+ onTransfer = (newOwner: UserInfo) => {
+ if (newOwner === null) {
+ message.info('Please select a user');
+ return;
+ }
+
+ const { domain, onOwnershipTransferred } = this.props;
+ const endpoint = `${process.env.NEXT_PUBLIC_FE_URL}/api/domain/transfer/${domain.id}`;
+ fetch(endpoint, {
+ method: 'POST',
+ body: newOwner.userId,
+ })
+ .then((response) => response.json())
+ .then((json: any) => {
+ switch (json.outcome) {
+ case 0:
+ message.info('Domain ownership has been transferred');
+ this.setState({ transferOwnershipModalVisible: false });
+ onOwnershipTransferred(newOwner);
+ break;
+ case 1:
+ message.error('You are not allowed to transfer the ownership of this domain');
+ this.setState({ transferOwnershipModalVisible: false });
+ break;
+ case 2:
+ message.error('Could not find the user to transfer ownership to');
+ break;
+ default:
+ message.error('Failed to transfer ownership due to an unexpected error');
+ throw Error('Failed to transfer ownership due to an unexpected error');
+ }
+ })
+ // eslint-disable-next-line no-console
+ .catch((e) => console.error(e));
+ };
+
+ /**
+ * Generate a list of options for a Select for selecting access levels.
+ * @returns Options for a Select to select the access level.
+ */
+ accessOptions = () => {
+ const options = [];
+ let ind = 0;
+ Object.keys(Access).forEach((key) => {
+ // Do not include the revoked and owner access level as a selectable option
+ if (key !== 'Revoked' && key !== 'Owner') {
+ ind += 1;
+ options.push((
+
+ ));
+ }
+ });
+ return options;
+ };
+
+ /**
+ * Define the columns of the table.
+ */
+ columns = (): ColumnsType => [
+ {
+ title: 'Name',
+ dataIndex: 'userDisplayName',
+ // Allow sorting by username
+ sorter: (a: UserWithAccess, b: UserWithAccess) => {
+ if (a.userDisplayName < b.userDisplayName) { return -1; }
+ if (a.userDisplayName > b.userDisplayName) { return 1; }
+ return 0;
+ },
+ sortDirections: ['ascend', 'descend'],
+ defaultSortOrder: 'ascend',
+ },
+ {
+ title: 'Role',
+ width: 250,
+ render: (userWithAccess: UserWithAccess) => {
+ const options = this.accessOptions();
+
+ return (
+
+ );
+ },
+ },
+ {
+ width: 100,
+ render: (userWithAccess: UserWithAccess) => (
+
+
+
+ ),
+ },
+ ];
+
+ render() {
+ const { domain } = this.props;
+ const { usersWithAccess, transferOwnershipModalVisible } = this.state;
+
+ /**
+ * Definition of the modal to transfer ownership.
+ */
+ const TransferOwnershipModal = () => {
+ const [newOwner, setNewOwner] = React.useState(null);
+
+ return (
+ this.setState({ transferOwnershipModalVisible: false })}
+ onOk={() => this.onTransfer(newOwner)}
+ >
+
+ You are about to transfer ownership of the domain to a different user.
+ You will still be able to use and edit the domain after the transfer,
+ but you will not be able to manage permissions anymore.
+
+ {/* The form is only being used for styling */}
+
+
+
+
+
+ }
+ onUserFound={this.onAddUser}
+ userValidation={this.validateAddUser}
+ ref={this.userSearchRef}
+ />
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default AccessManager;
diff --git a/front-end/src/components/Domain/DomainList/DomainList.tsx b/front-end/src/components/Domain/DomainList/DomainList.tsx
index 09d0193..993a76f 100644
--- a/front-end/src/components/Domain/DomainList/DomainList.tsx
+++ b/front-end/src/components/Domain/DomainList/DomainList.tsx
@@ -42,7 +42,7 @@ interface IState {
* The different variations are set via the `edit`, `showVisibility`, and `showAccess` props.
*/
class DomainList extends React.Component {
- /** React RefObject to ref to refer to the domain name search input */
+ /** React RefObject to refer to the domain name search input */
titleSearchRef: React.RefObject;
/**
@@ -107,7 +107,7 @@ class DomainList extends React.Component {
),
// Filter rule
onFilter: (val, record) => record.title.toString().toLowerCase().includes(val.toLowerCase()),
- // Select the search input after the search dropdown has openend
+ // Select the search input after the search dropdown has opened
onFilterDropdownVisibleChange: (visible: boolean) => {
if (visible) {
setTimeout(() => this.titleSearchRef.current.select(), 100);
diff --git a/front-end/src/components/UserSearch/UserSearch.tsx b/front-end/src/components/UserSearch/UserSearch.tsx
new file mode 100644
index 0000000..575eb44
--- /dev/null
+++ b/front-end/src/components/UserSearch/UserSearch.tsx
@@ -0,0 +1,83 @@
+import React, { CSSProperties, ReactNode, Ref } from 'react';
+import { Input, message } from 'antd';
+import UserInfo from '@models/User';
+
+const { Search } = Input;
+
+/**
+ * The props for the {@link UserSearch} component.
+ */
+interface UserSearchProps {
+ /** Placeholder text for the search field. */
+ placeholder?: string,
+ /**
+ * Whether to show an enter button after input, or use another React node.
+ * See Ant Design's documentation on Input.Search: https://ant.design/components/input/#Input.Search.
+ */
+ enterButton?: boolean | ReactNode,
+ /** Callback function when a user is found. */
+ onUserFound?: (user: UserInfo) => void,
+ /**
+ * Validate if a user may be selected.
+ * Return false if a user may not be selected, or true if a user may be selected.
+ */
+ userValidation?: (email: string) => boolean,
+ /** CSS styling for the internal Search component. */
+ style?: CSSProperties,
+}
+
+/**
+ * Component to search users by their e-mail address.
+ */
+const UserSearch = React.forwardRef((props: UserSearchProps, ref: Ref) => {
+ /**
+ * Search the user with the given e-mail address.
+ * @param mail The e-mail address to search by.
+ */
+ const searchUser = (mail: string) => {
+ const { userValidation } = props;
+ // If the user validation fails, don't continue.
+ if (userValidation(mail) === false) {
+ return;
+ }
+
+ const { onUserFound } = props;
+
+ const endpoint = `${process.env.NEXT_PUBLIC_FE_URL}/api/user/email/${mail}`;
+ fetch(endpoint, {
+ method: 'GET',
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data.found === false) {
+ message.error('Could not find a user with the given e-mail address');
+ return;
+ }
+ onUserFound(data);
+ });
+ };
+
+ const { placeholder, enterButton, style } = props;
+
+ return (
+
+ );
+});
+
+// Default values for UserSearchProps.
+UserSearch.defaultProps = {
+ placeholder: 'Search user by their email address',
+ enterButton: false,
+ onUserFound: () => {},
+ userValidation: () => true,
+ style: null,
+};
+
+export default UserSearch;
diff --git a/front-end/src/models/Domain.tsx b/front-end/src/models/Domain.tsx
index 3cb7d3f..878b926 100644
--- a/front-end/src/models/Domain.tsx
+++ b/front-end/src/models/Domain.tsx
@@ -61,3 +61,30 @@ export enum Access {
ReadWrite = 'ReadWrite',
Revoked = 'Revoked'
}
+
+/**
+ * An object received from the back-end when asked for all users with access to a domain.
+ * The relevant endpoint: `/api/domain/users-with-access/{id}`.
+ */
+export interface UserWithAccess {
+ /** The id of the UserDomainAccess object */
+ id: string,
+ /** The id of the user who has access. */
+ userId: string,
+ /** The display name of the user who has access. */
+ userDisplayName: string,
+ /** The id of the domain the user has access to. */
+ domainId: string,
+ /** The access level the user has to the domain. */
+ accessRight: Access,
+}
+
+/**
+ * Object to send user access to domain updates.
+ */
+export interface UserAccessUpload {
+ /** The id of the user who gains access. */
+ userId: string,
+ /** The access level the user will get to the domain. */
+ access: Access,
+}
diff --git a/front-end/src/models/User.tsx b/front-end/src/models/User.tsx
new file mode 100644
index 0000000..998eb2d
--- /dev/null
+++ b/front-end/src/models/User.tsx
@@ -0,0 +1,30 @@
+/**
+ * A user information received from the back-end.
+ */
+export default interface UserInfo {
+ /** The id of the user. */
+ userId: string,
+ /**
+ * The email address of the user.
+ * It might be null when the back-end hides it for privacy reasons.
+ */
+ email: string | null,
+ /** The display name of the user. */
+ displayName: string,
+ /** The status of the user account. */
+ status: UserStatus,
+ /** Whether the user is an administrator. */
+ isAdmin: boolean,
+}
+
+/**
+ * The account status of a user.
+ */
+export enum UserStatus {
+ /** The user account is approved. */
+ Approved = 'Approved',
+ /** The user account is pending approval. */
+ Pending = 'Pending',
+ /** The user account has been denied. */
+ Denied = 'Denied'
+}
diff --git a/front-end/src/pages/about.tsx b/front-end/src/pages/about.tsx
index 1e2a30d..2cdd56d 100644
--- a/front-end/src/pages/about.tsx
+++ b/front-end/src/pages/about.tsx
@@ -25,7 +25,7 @@ function AboutPage() {
About us
- APE Web View is a graphical interface for the APE library.
+ APE Web is a graphical interface for the APE library.
APE (Automated Pipeline Explorer) is a command line tool and Java API
for the automated exploration of possible computational pipelines
(scientific workflows) from large collections of computational tools.
diff --git a/front-end/src/pages/admin.tsx b/front-end/src/pages/admin.tsx
index 71466d3..3a8408b 100644
--- a/front-end/src/pages/admin.tsx
+++ b/front-end/src/pages/admin.tsx
@@ -14,6 +14,7 @@ import { getSession, signIn } from 'next-auth/client';
import TopicCreate from '@components/Admin/TopicCreate';
import RunParametersConfig from '@components/Admin/RunParametersConfig';
import { RunOptions } from '@models/workflow/Workflow';
+import PrivilegeManager from '@components/Admin/PrivilegeManager';
import styles from './Admin.module.less';
const { Title } = Typography;
@@ -58,7 +59,7 @@ class AdminPage extends React.Component {
motivation: value.motivation,
}));
- constructor(props) {
+ constructor(props: IProps) {
super(props);
// Initial state of the component: empty list
@@ -146,6 +147,10 @@ class AdminPage extends React.Component {
Run parameters configuration
+
+ Admin privilege management
+
+
);
}
diff --git a/front-end/src/pages/api/admin/adminstatus.tsx b/front-end/src/pages/api/admin/adminstatus.tsx
new file mode 100644
index 0000000..2e19a56
--- /dev/null
+++ b/front-end/src/pages/api/admin/adminstatus.tsx
@@ -0,0 +1,23 @@
+import { getSession } from 'next-auth/client';
+
+/**
+ * Handle incoming requests.
+ * @param req The incoming request.
+ * @param res The outgoing response.
+ */
+export default async function handler(req: any, res: any) {
+ const session: any = await getSession({ req });
+ const { body } = req;
+
+ const endpoint = `${process.env.NEXT_PUBLIC_BASE_URL_NODE}/api/admin/adminstatus`;
+ await fetch(endpoint, {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ cookie: session.user.sessionid,
+ },
+ body,
+ })
+ .then((response) => { res.status(response.status).end(); });
+}
diff --git a/front-end/src/pages/api/admin/user/index.tsx b/front-end/src/pages/api/admin/user/index.tsx
new file mode 100644
index 0000000..7d1f77e
--- /dev/null
+++ b/front-end/src/pages/api/admin/user/index.tsx
@@ -0,0 +1,43 @@
+import { getSession } from 'next-auth/client';
+import UserInfo from '@models/User';
+
+/**
+ * Handle GET requests.
+ * @param res The outgoing response.
+ * @param session The current session.
+ */
+async function handleGET(res: any, session: any) {
+ let users: UserInfo[];
+
+ const endpoint: string = `${process.env.NEXT_PUBLIC_BASE_URL_NODE}/user/`;
+ await fetch(endpoint, {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ cookie: session.user.sessionid,
+ },
+ })
+ .then((response) => response.json())
+ .then((data) => { users = data; });
+
+ return res.status(200).json(users);
+}
+
+/**
+ * Handle incoming requests.
+ * @param req The incoming request.
+ * @param res The outgoing response.
+ */
+export default async function handler(req: any, res: any) {
+ const session: any = await getSession({ req });
+ const { method } = req;
+
+ switch (method) {
+ case 'GET':
+ return handleGET(res, session);
+ default:
+ res.setHeader('Allow', ['GET']);
+ return res.status(405).end(`Method ${method} Not Allowed`);
+ }
+}
diff --git a/front-end/src/pages/api/domain/access/[...slug].tsx b/front-end/src/pages/api/domain/access/[...slug].tsx
new file mode 100644
index 0000000..1506e60
--- /dev/null
+++ b/front-end/src/pages/api/domain/access/[...slug].tsx
@@ -0,0 +1,85 @@
+import { UserAccessUpload, UserWithAccess } from '@models/Domain';
+import { getSession } from 'next-auth/client';
+
+/**
+ * GET the users with access to the domain and their access levels.
+ * @param res The outgoing response.
+ * @param session The current session.
+ * @param domainId The id of the domain to get users with access of it.
+ * @param accessLevels The access levels the users may have.
+ * @returns A response to the caller of this api endpoint.
+ */
+async function handleGET(res: any, session: any, domainId: string, accessLevels: any) {
+ let result: UserWithAccess[] | number;
+
+ const accessRights = accessLevels.join(',');
+ const endpoint = `${process.env.NEXT_PUBLIC_BASE_URL_NODE}/api/domain/users-with-access/${domainId}?accessRights=${accessRights}`;
+ await fetch(endpoint, {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ cookie: session.user.sessionid,
+ },
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ return response.status;
+ }
+ return response.json();
+ })
+ .then((data) => { result = data; });
+
+ // If an error occurred, return the HTTP status code.
+ if (typeof result === 'number') {
+ return res.status(result).end();
+ }
+ return res.status(200).json(result);
+}
+
+/**
+ * POST the access right of a user to the back-end.
+ * @param res The outgoing response.
+ * @param session The current session.
+ * @param domainId The id of the domain to which rights are given.
+ * @param userAccess The user and access level.
+ */
+async function handlePOST(res: any, session: any, domainId: string, userAccess: UserAccessUpload) {
+ let result: number;
+
+ const endpoint = `${process.env.NEXT_PUBLIC_BASE_URL_NODE}/api/domain/${domainId}/access`;
+ await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ cookie: session.user.sessionid,
+ },
+ body: JSON.stringify(userAccess),
+ })
+ .then((response) => { result = response.status; });
+
+ return res.status(result).end();
+}
+
+/**
+ * Handle incoming requests.
+ * @param req The incoming request.
+ * @param res The outgoing response.
+ * @returns A response to the caller of this api endpoint.
+ */
+export default async function handler(req: any, res: any) {
+ const session: any = await getSession({ req });
+ const { method } = req;
+ const { slug } = req.query;
+
+ switch (method) {
+ case 'GET':
+ return handleGET(res, session, slug[0], slug.slice(1, slug.length));
+ case 'POST': {
+ const { body } = req;
+ return handlePOST(res, session, slug[0], body);
+ }
+ default:
+ res.setHeader('Allow', ['GET', 'POST']);
+ return res.status(405).end(`Method ${method} Not Allowed`);
+ }
+}
diff --git a/front-end/src/pages/api/domain/transfer/[id].tsx b/front-end/src/pages/api/domain/transfer/[id].tsx
new file mode 100644
index 0000000..4a99fb1
--- /dev/null
+++ b/front-end/src/pages/api/domain/transfer/[id].tsx
@@ -0,0 +1,44 @@
+import { getSession } from 'next-auth/client';
+
+/**
+ * Handle incoming requests.
+ * @param req The incoming request.
+ * @param res The outgoing response.
+ */
+export default async function handler(req: any, res: any) {
+ const session: any = await getSession({ req });
+ const { id } = req.query;
+ const { body } = req;
+
+ const endpoint = `${process.env.NEXT_PUBLIC_BASE_URL_NODE}/api/domain/${id}/transfer/${body}`;
+ await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ cookie: session.user.sessionid,
+ },
+ })
+ .then((response) => {
+ switch (response.status) {
+ case 200:
+ // Ownership transferred
+ res.status(200).json({ outcome: 0 });
+ break;
+ case 403:
+ // User is not allowed to transfer the domain ownership.
+ res.status(200).json({ outcome: 1 });
+ break;
+ case 400:
+ /*
+ * User to transfer ownership to was not found, or the domain was not found
+ * (but this is highly unlikely because the domain id was handled automatically).
+ */
+ res.status(200).json({ outcome: 2 });
+ break;
+ default:
+ res.status(response.status).json();
+ break;
+ }
+ })
+ .catch(() => res.status(500).end());
+}
diff --git a/front-end/src/pages/api/user/email/[email].tsx b/front-end/src/pages/api/user/email/[email].tsx
new file mode 100644
index 0000000..aa58646
--- /dev/null
+++ b/front-end/src/pages/api/user/email/[email].tsx
@@ -0,0 +1,46 @@
+import { getSession } from 'next-auth/client';
+
+/**
+ * Handle GET requests.
+ * @param req The incoming request.
+ * @param res The outgoing response.
+ * @param session The current session.
+ */
+async function handleGET(req: any, res: any, session: any) {
+ const { email } = req.query;
+ const endpoint: string = `${process.env.NEXT_PUBLIC_BASE_URL_NODE}/user/email/${email}`;
+ await fetch(endpoint, {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ cookie: session.user.sessionid,
+ },
+ })
+ .then(async (response) => {
+ if (!response.ok) {
+ const message = await response.json().then((e) => e.message);
+ throw new Error(message);
+ }
+ return response.json();
+ })
+ .then((data) => res.status(200).json(data))
+ .catch(() => res.status(200).json({ found: false }));
+}
+
+/**
+ * Handle incoming requests.
+ * @param req The incoming request.
+ * @param res The outgoing response.
+ */
+export default async function handler(req: any, res: any) {
+ const session: any = await getSession({ req });
+ const { method } = req;
+
+ switch (method) {
+ case 'GET':
+ return handleGET(req, res, session);
+ default:
+ res.setHeader('Allow', ['GET']);
+ return res.status(405).end(`Method ${method} Not Allowed`);
+ }
+}
diff --git a/front-end/src/pages/contact.tsx b/front-end/src/pages/contact.tsx
index 6d5ccb6..c69fba6 100644
--- a/front-end/src/pages/contact.tsx
+++ b/front-end/src/pages/contact.tsx
@@ -26,7 +26,7 @@ function ContactPage() {
Contact
- For any questions concerning APE and APE Web View you can get in touch with:
+ For any questions concerning APE and APE Web you can get in touch with:
Vedran Kasalica (v.kasalica[at]uu.nl), lead developer
diff --git a/front-end/src/pages/domain/edit/[id].tsx b/front-end/src/pages/domain/edit/[id].tsx
index f45ddab..c6d8f45 100644
--- a/front-end/src/pages/domain/edit/[id].tsx
+++ b/front-end/src/pages/domain/edit/[id].tsx
@@ -8,22 +8,32 @@
import React from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
-import { Button, Result } from 'antd';
+import { Button, Result, Typography } from 'antd';
import DomainEdit from '@components/Domain/DomainEdit/DomainEdit';
-import Domain, { Topic } from '@models/Domain';
+import Domain, { Topic, UserWithAccess } from '@models/Domain';
import { getSession } from 'next-auth/client';
import { fetchTopics } from '@components/Domain/Domain';
+import AccessManager from '@components/Domain/AccessManager/AccessManager';
import styles from './[id].module.less';
+const { Title } = Typography;
+
/**
* Props for DomainEditPage
*/
interface IDomainEditPageProps {
+ /** The ID of the current user. */
+ userId: string,
/** The ID of the domain to edit */
domain: Domain;
+ /** The topics of the domain. */
topics: Topic[];
+ /** Whether the domain was found. */
notFound: boolean;
+ /** Whether the user has access to the domain. */
access: boolean;
+ /** Whether the user is the owner of the domain. */
+ isOwner: boolean;
}
/**
@@ -57,6 +67,33 @@ async function hasAccess(user, domainID: string): Promise {
return access;
}
+/**
+ * Check whether a user is the owner of a domain.
+ * @param user The user who we check to be the owner.
+ * @param domainId The domain to check ownership of.
+ * @returns True if the user is the owner, false if the user is not the owner.
+ */
+async function checkOwnership(user, domainId: string): Promise {
+ const endpoint = `${process.env.NEXT_PUBLIC_BASE_URL_NODE}/api/domain/users-with-access/${domainId}?accessRights=Owner`;
+ return fetch(endpoint, {
+ method: 'GET',
+ headers: {
+ cookie: user.sessionid,
+ },
+ })
+ .then((response) => response.json())
+ .then((data: UserWithAccess[]) => {
+ let owner = false;
+ data.forEach((u: UserWithAccess) => {
+ if (u.userId === user.userId) {
+ owner = true;
+ }
+ });
+ return owner;
+ })
+ .catch(() => false);
+}
+
/**
* Fetch a domain from the back-end.
* @param user The user information, with the sessionid.
@@ -82,27 +119,41 @@ async function fetchDomain(user: any, id: string): Promise {
/**
* Page for editing domains, built around the {@link DomainEdit} component.
*
- * Includes result pages to be shown in case of erorrs.
+ * Includes result pages to be shown in case of errors.
*/
-function DomainEditPage({ domain, topics, notFound, access }: IDomainEditPageProps) {
+function DomainEditPage(props: IDomainEditPageProps) {
const router = useRouter();
+ const { userId, domain, topics, notFound, access, isOwner: isOwnerInitial } = props;
+ const [isOwner, setIsOwner] = React.useState(isOwnerInitial);
return (
{ /*
* Make sure DomainEdit is not rendered before data is loaded.
- * Otherwise, Ant Desing's Form initialValues does not work.
+ * Otherwise, Ant Design's Form initialValues does not work.
*/
domain !== null && topics !== [] && access && (
)
}
@@ -122,13 +173,13 @@ function DomainEditPage({ domain, topics, notFound, access }: IDomainEditPagePro
)
}
{
- // Show unauthorized result when the user doesn not have access
+ // Show unauthorized result when the user doesn't not have access
!access && (
Return to my domains}
+ extra={}
/>
)
}
@@ -137,29 +188,35 @@ function DomainEditPage({ domain, topics, notFound, access }: IDomainEditPagePro
}
export async function getServerSideProps({ query, req }) {
- // Get the domainID fromt the url parameters
+ // Get the domainID from the url parameters
const session = await getSession({ req });
- let access;
+ let access: boolean = false;
+ let owner: boolean = false;
let notFound = false;
let domain = null;
let topics = [];
- await hasAccess(session.user, query.id).then((acc) => { access = acc; });
- if (access) {
- await fetchDomain(session.user, query.id)
- .then((d) => {
- // Domain not found, update state
- if (d === null) {
- notFound = true;
- return;
- }
- domain = d;
- });
- // Get all topics
- await fetchTopics(session.user, true)
- .then((t) => { topics = t; });
+
+ if (session !== null) {
+ await hasAccess(session.user, query.id).then((acc) => { access = acc; });
+ if (access) {
+ await fetchDomain(session.user, query.id)
+ .then((d) => {
+ // Domain not found, update state
+ if (d === null) {
+ notFound = true;
+ return;
+ }
+ domain = d;
+ });
+ // Get all topics
+ await fetchTopics(session.user, true)
+ .then((t) => { topics = t; });
+ await checkOwnership(session.user, query.id)
+ .then((o) => { owner = o; });
+ }
}
return {
- props: { access, notFound, domain, topics },
+ props: { access, isOwner: owner, notFound, domain, topics },
};
}
diff --git a/front-end/src/pages/index.tsx b/front-end/src/pages/index.tsx
index 5f9288a..e1a634e 100644
--- a/front-end/src/pages/index.tsx
+++ b/front-end/src/pages/index.tsx
@@ -73,10 +73,10 @@ function DomainsPage({ publicDomains, ownedDomains, sharedDomains, session }: ID
Home | APE
- Welcome to APE Web View
+ Welcome to APE Web
- APE (Automated Pipeline Explorer) Web View is a graphical interface for the
+ APE (Automated Pipeline Explorer) Web is a graphical interface for the
APE library
(see GitHub),
used for the automated exploration of possible computational pipelines
@@ -93,7 +93,7 @@ function DomainsPage({ publicDomains, ownedDomains, sharedDomains, session }: ID
our page.
- APE Web View allows you to explore and automatically compose
+ APE Web allows you to explore and automatically compose
these workflows from pre-defined domains
(such as image manipulation domain using the ImageMagick toolset).
In addition you are encouraged to create your own domains and share them with other users.
diff --git a/front-end/src/pages/privacy.tsx b/front-end/src/pages/privacy.tsx
index d22683a..ca295e4 100644
--- a/front-end/src/pages/privacy.tsx
+++ b/front-end/src/pages/privacy.tsx
@@ -23,12 +23,12 @@ function PrivacyPage() {
- APE Web View Privacy Policy
+ APE Web Privacy Policy
- At APE WEb View, accessible at ape.science.uu.nl,
+ At APE Web, accessible at ape.science.uu.nl,
one of our main priorities is the privacy of our visitors.
This Privacy Policy document contains types of information
- that is collected and recorded by the APE Web View website and how we use it.
+ that is collected and recorded by the APE Web website and how we use it.
If you have additional questions or require more information about our Privacy Policy,
@@ -38,7 +38,7 @@ function PrivacyPage() {
This privacy policy applies only to our online activities and is valid for
visitors to our website with regards to the information that they shared and/or
- collect in APE Web View.
+ collect in APE Web.
This policy is not applicable to any information collected offline
or via channels other than this website.