Commit 350337ad authored by Martin Lowe's avatar Martin Lowe 🇨🇦
Browse files

Merge branch 'dev' into 'master'

Prep for release 1.3.4

See merge request !490
parents fd41b74b 08bab538
Pipeline #1900 passed with stage
in 0 seconds
package org.eclipsefoundation.react.config;
import java.util.List;
import java.util.Map;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
@ConfigMapping(prefix = "eclipse.cors")
public interface EnhancedCORSConfig {
@WithDefault("false")
boolean enabled();
Map<String, EnhancedCORSSet> config();
public static interface EnhancedCORSSet {
List<String> endpointPatterns();
List<String> origins();
List<String> methods();
}
}
package org.eclipsefoundation.react.request;
import java.io.IOException;
import java.util.Map.Entry;
import java.util.Optional;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.ext.Provider;
import org.eclipsefoundation.react.config.EnhancedCORSConfig;
import org.eclipsefoundation.react.config.EnhancedCORSConfig.EnhancedCORSSet;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.quarkus.security.ForbiddenException;
import io.quarkus.vertx.http.runtime.cors.CORSFilter;
/**
* Hooks into the configured CORS Filter to allow multiple configuration sets for different endpoints. This will allow a
* more advanced configuration of the CORS behaviour of the endpoints, but will lose some of the more precise logging
* available to standard CORS implementations (we can't pinpoint the target configuration so we can't inform the user on
* how to fix the issue).
*
* @author Martin Lowe
*
*/
@Provider
public class EnhancedCORSFilter implements ContainerRequestFilter {
public static final Logger LOGGER = LoggerFactory.getLogger(EnhancedCORSFilter.class);
@Inject
Instance<EnhancedCORSConfig> enhancedCORSHandle;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
EnhancedCORSConfig enhancedCORS = enhancedCORSHandle.get();
if (enhancedCORS.enabled()) {
// if the origin is not set then request cannot be processed with CORS and should fall back to builtin
String origin = requestContext.getHeaderString(HttpHeaderNames.ORIGIN.toString());
if (origin == null) {
LOGGER.trace("Request has no origin, will fall back to built-in CORS support");
return;
}
String method = requestContext.getMethod();
String uri = requestContext.getUriInfo().getPath();
// iterate over sets looking for a match
for (Entry<String, EnhancedCORSSet> e : enhancedCORS.config().entrySet()) {
// check if we can skip the CORS config set based on methods, with allowed wildcard
if (e.getValue().methods().stream().noneMatch(m -> m.equalsIgnoreCase(method))
&& e.getValue().methods().stream().noneMatch(m -> m.equalsIgnoreCase("*"))) {
LOGGER.trace("Config set {} methods do not match current method {} ({})", e.getKey(), method,
e.getValue().methods());
continue;
}
// check if any of the endpoints match exactly, or are matching regexes
// makes use of conversion from CORSFilter for origins for simplicity
if (e.getValue().endpointPatterns().stream().noneMatch(endpoint -> uri.equalsIgnoreCase(endpoint))
&& CORSFilter.parseAllowedOriginsRegex(Optional.of(e.getValue().endpointPatterns())).stream()
.noneMatch(p -> p.matcher(uri).matches())) {
LOGGER.trace("Config set {} endpoint patterns do not match current uri {} ({})", e.getKey(), uri,
e.getValue().endpointPatterns());
continue;
}
// check if the origin matches the current set
if (e.getValue().origins().stream().noneMatch(m -> m.equalsIgnoreCase(origin))
&& !CORSFilter.isOriginAllowedByRegex(
CORSFilter.parseAllowedOriginsRegex(Optional.of(e.getValue().origins())), origin)) {
LOGGER.trace("Config set {} origins do not match current origin {} ({})", e.getKey(), origin,
e.getValue().origins());
continue;
}
// current config set matches, log and move forward
LOGGER.trace("Request by {} to {} {} passes internal CORS config with name {}", origin, method, uri,
e.getKey());
return;
}
// kept at debug to reduce log spam but expose it before the informational logging
LOGGER.debug(
"Could not find a matching CORS config for current request origin {} to endpoint {} {}, aborting",
origin, method, uri);
throw new ForbiddenException(Messages.MESSAGES.originNotAllowed(origin));
}
}
}
......@@ -470,12 +470,8 @@ public class OrganizationResource extends AbstractRESTResource {
}
@GET
@Authenticated
@RolesAllowed({ CR, DE, CRA, MA })
@Path("{orgID:\\d+}/products")
public Response getProducts(@PathParam("orgID") String organizationID,
@HeaderParam(value = CSRFHelper.CSRF_HEADER_NAME) String csrf) {
csrfHelper.compareCSRF(aud, csrf);
public Response getProducts(@PathParam("orgID") String organizationID) {
// limit results to given org
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add(EclipseDBParameterNames.ORGANIZATION_ID.getName(), organizationID);
......@@ -503,12 +499,8 @@ public class OrganizationResource extends AbstractRESTResource {
}
@GET
@Authenticated
@RolesAllowed({ CR, DE, CRA, MA })
@Path("{orgID:\\d+}/products/{productID:\\d+}")
public Response getProduct(@PathParam("orgID") Integer organizationID, @PathParam("productID") Integer productID,
@HeaderParam(value = CSRFHelper.CSRF_HEADER_NAME) String csrf) {
csrfHelper.compareCSRF(aud, csrf);
public Response getProduct(@PathParam("orgID") Integer organizationID, @PathParam("productID") Integer productID) {
// limit results to given org
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add(EclipseDBParameterNames.ORGANIZATION_ID.getName(), Integer.toString(organizationID));
......
quarkus.log.level=INFO
quarkus.http.port=8090
quarkus.http.cors=true
quarkus.http.cors.origins=https://membership.eclipse.org,https://membership-staging.eclipse.org
quarkus.http.cors.origins=*
quarkus.http.cors.exposed-headers=x-csrf-token
quarkus.http.cors.headers=x-csrf-token
quarkus.cache.caffeine."default".expire-after-write=30M
quarkus.cache.caffeine."ttl".expire-after-write=${quarkus.cache.caffeine."default".expire-after-write}
## Enhanced cors
eclipse.cors.enabled=true
eclipse.cors.config.frontend-access.endpoint-patterns=/.*/
eclipse.cors.config.frontend-access.methods=*
eclipse.cors.config.frontend-access.origins=https://membership.eclipse.org,https://membership-staging.eclipse.org
eclipse.cors.config.organizations.endpoint-patterns=/^\\\\/api\\\\/organizations.*/
eclipse.cors.config.organizations.methods=GET
eclipse.cors.config.organizations.origins=*
## IMAGE UPLOAD SETTINGS - REQUIRED FOR MULTIPART
quarkus.http.body.handle-file-uploads=true
quarkus.http.body.delete-uploaded-files-on-end=true
......
[
{
"relation": "AC",
"description": "Architecture Council Representative",
"active": true,
"type": "CO",
"sort_order": ""
},
{ "relation": "AD", "description": "Admin Contact", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "BA", "description": "Board Rep - Alternate", "active": true, "type": "CO", "sort_order": "" },
{
"relation": "BABE",
"description": "Board Rep - EF-BE - Alternate",
"active": true,
"type": "CO",
"sort_order": ""
},
{ "relation": "BR", "description": "Board Rep", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "BRBE", "description": "EF-BE Board Rep - EF-BE", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "BRUS", "description": "EF-US Board Rep - EF-US", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "CA", "description": "Accounting Contact", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "CC", "description": "Committer Member", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "CI", "description": "IT Contact", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "CL", "description": "Legal Contact", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "CR", "description": "Member Representative", "active": true, "type": "CO", "sort_order": "" },
{
"relation": "CRA",
"description": "Member Representative - Alternate",
"active": true,
"type": "CO",
"sort_order": ""
},
{ "relation": "DE", "description": "Delegate", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "EMPLY", "description": "Company Employee", "active": true, "type": "CO", "sort_order": "" },
{ "relation": "MA", "description": "Marketing", "active": true, "type": "CO", "sort_order": "" },
{
"relation": "PC",
"description": "Planning Council Representative",
"active": true,
"type": "CO",
"sort_order": ""
},
{
"relation": "SDCP",
"description": "Contact person for 24/7 IT support",
"active": true,
"type": "CO",
"sort_order": ""
},
{ "relation": "WGR", "description": "Working Group Representative", "active": true, "type": "CO", "sort_order": "" },
{
"relation": "WGSC",
"description": "Working Group Steering Committee Representative",
"active": true,
"type": "CO",
"sort_order": ""
}
]
......@@ -168,5 +168,39 @@
"disk_quota_gb": 2,
"component": false,
"standard": false
},
{
"project_id": "modeling.mdt.xsd",
"name": "XSD",
"level": 3,
"parent_project_id": "modeling.mdt",
"description": "",
"url_download": "http://download.eclipse.org/modeling/mdt/xsd",
"url_index": "http://www.eclipse.org/modeling/mdt/?project=xsd",
"date_active": null,
"sort_order": 0,
"active": true,
"bugs_name": "MDT.XSD",
"project_phase": "Regular",
"disk_quota_gb": 2.0,
"component": false,
"standard": false
},
{
"project_id": "technology.dash",
"name": "Dash, Tools for Committers",
"level": 2,
"parent_project_id": "technology",
"description": "",
"url_download": "http://download.eclipse.org/technology/dash",
"url_index": "http://www.eclipse.org/dash/",
"date_active": null,
"sort_order": 0,
"active": true,
"bugs_name": "Dash",
"project_phase": "Incubation",
"disk_quota_gb": 2.0,
"component": false,
"standard": false
}
]
......@@ -217,6 +217,7 @@ export const NAV_PATHS = {
dashboardResources: '/portal/dashboard#resources',
dashboardFAQs: '/portal/dashboard#faqs',
contactManagement: '/portal/contact-management',
yourProjects: '/portal/your-projects',
};
export const NAV_OPTIONS_DATA = [
......@@ -260,6 +261,11 @@ export const NAV_OPTIONS_DATA = [
path: NAV_PATHS.contactManagement,
icon: <RecentActorsIcon />,
},
{
name: 'Your Projects',
path: NAV_PATHS.yourProjects,
icon: <BusinessCenterIcon />,
},
{
name: 'Your Organization Profile',
path: NAV_PATHS.orgProfile,
......
......@@ -9,6 +9,7 @@ import {
OrgRep,
ProjectsAndWGItem,
ResourcesData,
projectsDataFrontend,
} from '../Interfaces/portal_interface';
interface PortalContextType {
......@@ -42,8 +43,8 @@ interface PortalContextType {
setInterestedProjectsData: (interestedProjectsData: Array<ProjectsAndWGItem>) => void;
yourProjectsData: Array<ProjectsAndWGItem> | null;
setYourProjectsData: (yourProjectsData: Array<ProjectsAndWGItem>) => void;
allYourProjectsData: Array<ProjectsAndWGItem> | null;
setAllYourProjectsData: (allYourProjectsData: Array<ProjectsAndWGItem>) => void;
allYourProjectsData: Array<projectsDataFrontend> | null;
setAllYourProjectsData: (allYourProjectsData: Array<projectsDataFrontend>) => void;
yourWGData: Array<ProjectsAndWGItem> | null;
setYourWGData: (yourWGData: Array<ProjectsAndWGItem>) => void;
allYourWGData: Array<ProjectsAndWGItem> | null;
......@@ -86,7 +87,7 @@ const PortalContext = React.createContext<PortalContextType>({
yourProjectsData: null,
setYourProjectsData: (yourProjectsData: Array<ProjectsAndWGItem>) => {},
allYourProjectsData: null,
setAllYourProjectsData: (allYourProjectsData: Array<ProjectsAndWGItem>) => {},
setAllYourProjectsData: (allYourProjectsData: Array<projectsDataFrontend>) => {},
yourWGData: null,
setYourWGData: (yourWGData: Array<ProjectsAndWGItem>) => {},
allYourWGData: null,
......
......@@ -93,3 +93,24 @@ export interface CBIData {
inUse: number;
allocated: number;
}
export interface projectsDataBackend {
active: boolean;
level: number;
project_id: string;
name: string;
url_index: string;
url_download: string;
}
export interface projectsDataFrontend {
status: string;
id: string;
name: string;
url: string;
}
export interface allProjectsBackend {
name: string;
url: string;
}
......@@ -53,10 +53,6 @@ const useStyle = makeStyles((theme: Theme) =>
pageTitle: {
margin: theme.spacing(0.5, 0, 4),
},
tableIcon: {
fontSize: 50,
color: iconGray,
},
contactFilterText: {
backgroundColor: brightOrange,
color: 'white',
......@@ -65,7 +61,8 @@ const useStyle = makeStyles((theme: Theme) =>
borderTopRightRadius: borderRadiusSize,
},
tableContainer: {
height: 420,
minHeight: 320,
height: 'calc(100vh - 510px)',
width: '100%',
display: 'flex',
justifyContent: 'center',
......@@ -470,7 +467,9 @@ export default function ContactManagement() {
};
if (allRelations === null) {
fetchWrapperPagination(api_prefix() + '/sys/relations?type=CO&page=', 1, saveAllRelations);
isReactOnlyMode
? fetchWrapper('/membership_data/test_contact_relations.json', FETCH_METHOD.GET, saveAllRelations)
: fetchWrapperPagination(api_prefix() + '/sys/relations?type=CO&page=', 1, saveAllRelations);
}
}, [allRelations, setAllRelations]);
......@@ -502,7 +501,6 @@ export default function ContactManagement() {
<Typography className={classes.pageTitle} variant="h4" component="h1">
Contact Management
</Typography>
<RecentActorsIcon className={classes.tableIcon} />
<Typography variant="body1" className={classes.contactFilterText}>
Contacts
</Typography>
......
......@@ -5,6 +5,7 @@ import {
FETCH_METHOD,
getCurrentMode,
MODE_REACT_ONLY,
NAV_PATHS,
} from '../../../../Constants/Constants';
import CustomCard from '../../../UIComponents/CustomCard/CustomCard';
import { useEffect, useContext, useState } from 'react';
......@@ -12,11 +13,20 @@ import BusinessCenterIcon from '@material-ui/icons/BusinessCenter';
import { fetchWrapper, fetchWrapperPagination } from '../../../../Utils/formFunctionHelpers';
import { pickRandomItems } from '../../../../Utils/portalFunctionHelpers';
import PortalContext from '../../../../Context/PortalContext';
import { DashboardProjectsAndWGProps } from '../../../../Interfaces/portal_interface';
import {
allProjectsBackend,
DashboardProjectsAndWGProps,
projectsDataFrontend,
} from '../../../../Interfaces/portal_interface';
import { useHistory } from 'react-router';
const isReactOnlyMode = getCurrentMode() === MODE_REACT_ONLY;
export default function DashboardProjects({ setSelectedItemArray, setOpen }: DashboardProjectsAndWGProps) {
const history = useHistory();
const [isFetchingYourProjects, setIsFetchingYourProjects] = useState(true);
const [isFetchingInterestedProjects, setIsFetchingInterestedProjects] = useState(true);
const [allYourActiveProjects, setAllYourActiveProjects] = useState<null | Array<projectsDataFrontend>>(null);
const {
orgId,
yourProjectsData,
......@@ -28,56 +38,53 @@ export default function DashboardProjects({ setSelectedItemArray, setOpen }: Das
} = useContext(PortalContext);
useEffect(() => {
if (yourProjectsData !== null && allYourProjectsData !== null) {
setIsFetchingYourProjects(false);
}
if (interestedProjectsData !== null) {
setIsFetchingInterestedProjects(false);
}
if (!orgId || yourProjectsData !== null || interestedProjectsData !== null || allYourProjectsData !== null) {
if (allYourProjectsData !== null || !orgId) {
return;
}
const isReactOnlyMode = getCurrentMode() === MODE_REACT_ONLY;
let allYourProjects: Array<any> = [];
// For your projects data
const urlForYourProjects = isReactOnlyMode
? '/membership_data/test_your_projects.json'
: api_prefix() + `/${END_POINT.organizations}/${orgId}/${END_POINT.projects}?page=`;
const saveYourProjectsData = (data: Array<any>) => {
const saveAllYourProjectsData = (data: Array<any>) => {
if (data.length === 0) {
// When the list is empty, show this message in the card
data = [{ name: 'No projects yet' }];
}
allYourProjects = data
.filter((project) => project.active)
.map((project) => ({
name: project.name,
url: project.url_index || project.url_download || '',
}));
const allYourProjects = data.map((project) => ({
name: project.name,
url: `https://projects.eclipse.org/projects/${project.project_id}`,
id: project.project_id,
status: project.active ? 'Active' : 'Inactive',
}));
setAllYourProjectsData(allYourProjects);
const fourRandomItems = pickRandomItems(allYourProjects, 4);
setYourProjectsData(fourRandomItems);
setIsFetchingYourProjects(false);
const urlForAllProjects = isReactOnlyMode
? '/membership_data/test_all_projects.json'
: `https://projects.eclipse.org/api/projects?order_by=random&pagesize=${4 + allYourProjects.length}`;
fetchWrapper(urlForAllProjects, FETCH_METHOD.GET, saveInterestedProjectsData);
setAllYourActiveProjects(allYourProjects.filter((project) => project.status === 'Active'));
};
if (isReactOnlyMode) {
fetchWrapper(urlForYourProjects, FETCH_METHOD.GET, saveYourProjectsData);
fetchWrapper(urlForYourProjects, FETCH_METHOD.GET, saveAllYourProjectsData);
} else {
fetchWrapperPagination(urlForYourProjects, 1, saveYourProjectsData);
fetchWrapperPagination(urlForYourProjects, 1, saveAllYourProjectsData);
}
}, [orgId, allYourProjectsData, setAllYourProjectsData]);
useEffect(() => {
if (allYourActiveProjects === null) {
allYourProjectsData &&
setAllYourActiveProjects(allYourProjectsData.filter((project) => project.status === 'Active'));
return;
}
if (yourProjectsData !== null || interestedProjectsData !== null) {
yourProjectsData && setIsFetchingYourProjects(false);
interestedProjectsData && setIsFetchingInterestedProjects(false);
return;
}
// For interested projects data
const saveInterestedProjectsData = (data: Array<any>) => {
const saveInterestedProjectsData = (data: Array<allProjectsBackend>) => {
const allProjects = data.map((project) => ({
name: project.name,
url: project.url,
......@@ -85,20 +92,33 @@ export default function DashboardProjects({ setSelectedItemArray, setOpen }: Das
// filter out interested ones
const interestedProjects = allProjects.filter(
(project) => !allYourProjects.find((item) => item.name === project.name)
(project) => !allYourActiveProjects.find((item) => item.name === project.name)
);
const fourRandomItems = pickRandomItems(interestedProjects, 4);
setInterestedProjectsData(fourRandomItems);
setIsFetchingInterestedProjects(false);
};
const saveFourRandomProjects = () => {
const fourRandomItems = pickRandomItems(allYourActiveProjects, 4);
setYourProjectsData(fourRandomItems);
setIsFetchingYourProjects(false);
const urlForAllProjects = isReactOnlyMode
? '/membership_data/test_all_projects.json'
: `https://projects.eclipse.org/api/projects?order_by=random&pagesize=${4 + allYourActiveProjects.length}`;
fetchWrapper(urlForAllProjects, FETCH_METHOD.GET, saveInterestedProjectsData);
};
saveFourRandomProjects();
}, [
orgId,
allYourProjectsData,
setAllYourProjectsData,
yourProjectsData,
setYourProjectsData,
allYourActiveProjects,
interestedProjectsData,
setInterestedProjectsData,
yourProjectsData,
setYourProjectsData,
]);
return (
......@@ -109,10 +129,9 @@ export default function DashboardProjects({ setSelectedItemArray, setOpen }: Das
color={darkOrange}
icon={<BusinessCenterIcon />}
listItems={yourProjectsData || []}
urlText={allYourProjectsData && allYourProjectsData.length > 4 ? 'View all' : ''}
urlText={allYourActiveProjects && allYourActiveProjects.length > 4 ? 'View all' : ''}
callBackFunc={() => {
setSelectedItemArray({ title: 'View All Your Projects', data: allYourProjectsData || [] });
setOpen(true);
history.push(NAV_PATHS.yourProjects);
}}
/>
<CustomCard
......
......@@ -19,6 +19,7 @@ import { useEffect, useContext, useState } from 'react';
import GlobalContext from '../../Context/GlobalContext';
import PortalFooter from './PortalFooter';
import PortalLogin from './Login/PortalLogin';
import YourProjects from './YourProjects/YourProjects';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
......@@ -27,6 +28,7 @@ const useStyles = makeStyles((theme: Theme) =>
},
content: {
position: 'relative',
minHeight: 'calc(100vh - 65px)',
flexGrow: 1,
padding: theme.spacing(2.5, 1.5, 13.5),