mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 08:55:44 +01:00
Show session expired warning if the jwt token is expired
This commit is contained in:
@@ -38,6 +38,7 @@ import sonia.scm.SCMContext;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.security.AnonymousMode;
|
||||
import sonia.scm.security.AnonymousToken;
|
||||
import sonia.scm.security.BearerToken;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.util.Util;
|
||||
import sonia.scm.web.WebTokenGenerator;
|
||||
@@ -49,7 +50,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
import static sonia.scm.web.filter.JwtValidator.isJwtTokenExpired;
|
||||
|
||||
/**
|
||||
* Handles authentication, if a one of the {@link WebTokenGenerator} returns
|
||||
@@ -61,23 +62,16 @@ import java.util.Set;
|
||||
@Singleton
|
||||
public class AuthenticationFilter extends HttpFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
|
||||
|
||||
/**
|
||||
* marker for failed authentication
|
||||
*/
|
||||
private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed";
|
||||
|
||||
/**
|
||||
* Field description
|
||||
*/
|
||||
private static final String HEADER_AUTHORIZATION = "Authorization";
|
||||
|
||||
/**
|
||||
* the logger for AuthenticationFilter
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(AuthenticationFilter.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
private final Set<WebTokenGenerator> tokenGenerators;
|
||||
protected ScmConfiguration configuration;
|
||||
|
||||
/**
|
||||
* Constructs a new basic authenticaton filter.
|
||||
@@ -91,8 +85,6 @@ public class AuthenticationFilter extends HttpFilter {
|
||||
this.tokenGenerators = tokenGenerators;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handles authentication, if a one of the {@link WebTokenGenerator} returns
|
||||
* an {@link AuthenticationToken}.
|
||||
@@ -104,14 +96,15 @@ public class AuthenticationFilter extends HttpFilter {
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain chain)
|
||||
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
AuthenticationToken token = createToken(request);
|
||||
|
||||
if (token != null) {
|
||||
if (token instanceof BearerToken && isJwtTokenExpired(((BearerToken) token).getCredentials())) {
|
||||
handleUnauthorized(request, response, chain);
|
||||
} else if (token != null) {
|
||||
logger.trace(
|
||||
"found authentication token on request, start authentication");
|
||||
handleAuthentication(request, response, chain, subject, token);
|
||||
@@ -173,11 +166,8 @@ public class AuthenticationFilter extends HttpFilter {
|
||||
* @param response http response
|
||||
* @throws IOException
|
||||
*/
|
||||
protected void sendUnauthorizedError(HttpServletRequest request,
|
||||
HttpServletResponse response)
|
||||
throws IOException {
|
||||
HttpUtil.sendUnauthorized(request, response,
|
||||
configuration.getRealmDescription());
|
||||
protected void sendUnauthorizedError(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
HttpUtil.sendUnauthorized(request, response, configuration.getRealmDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,8 +250,6 @@ public class AuthenticationFilter extends HttpFilter {
|
||||
response);
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns {@code true} if anonymous access is enabled.
|
||||
*
|
||||
@@ -270,16 +258,4 @@ public class AuthenticationFilter extends HttpFilter {
|
||||
private boolean isAnonymousAccessEnabled() {
|
||||
return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* set of web token generators
|
||||
*/
|
||||
private final Set<WebTokenGenerator> tokenGenerators;
|
||||
|
||||
/**
|
||||
* scm main configuration
|
||||
*/
|
||||
protected ScmConfiguration configuration;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.web.filter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
|
||||
public class JwtValidator {
|
||||
|
||||
private JwtValidator() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the jwt token is expired.
|
||||
*
|
||||
* @return {@code true}if the token is expired
|
||||
*/
|
||||
public static boolean isJwtTokenExpired(String raw) {
|
||||
|
||||
boolean expired = false;
|
||||
|
||||
String[] parts = raw.split("\\.");
|
||||
|
||||
if (parts.length > 1) {
|
||||
Base64.Decoder decoder = Base64.getUrlDecoder();
|
||||
String payload = new String(decoder.decode(parts[1]));
|
||||
String[] splitJwt = payload.split(",");
|
||||
|
||||
for (String entry : splitJwt) {
|
||||
if (entry.contains("\"exp\"")) {
|
||||
long expirationTime = Long.parseLong(entry.replaceAll("[^\\d.]", ""));
|
||||
|
||||
if (Instant.now().isAfter(Instant.ofEpochSecond(expirationTime))) {
|
||||
expired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return expired;
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,6 @@
|
||||
|
||||
package sonia.scm.web.filter;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -38,6 +36,7 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.security.BearerToken;
|
||||
import sonia.scm.web.WebTokenGenerator;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@@ -47,191 +46,98 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/shiro.ini")
|
||||
public class AuthenticationFilterTest
|
||||
{
|
||||
public class AuthenticationFilterTest {
|
||||
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Mock
|
||||
private FilterChain chain;
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
@Mock
|
||||
private HttpServletResponse response;
|
||||
|
||||
private ScmConfiguration configuration;
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void testDoFilterAuthenticated() throws IOException, ServletException
|
||||
{
|
||||
public void testDoFilterAuthenticated() throws IOException, ServletException {
|
||||
AuthenticationFilter filter = createAuthenticationFilter();
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
verify(chain).doFilter(any(HttpServletRequest.class),
|
||||
any(HttpServletResponse.class));
|
||||
verify(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Test
|
||||
public void testDoFilterUnauthorized() throws IOException, ServletException
|
||||
{
|
||||
public void testDoFilterUnauthorized() throws IOException, ServletException {
|
||||
AuthenticationFilter filter = createAuthenticationFilter();
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED,
|
||||
"Authorization Required");
|
||||
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Test
|
||||
public void testDoFilterWithAuthenticationFailed()
|
||||
throws IOException, ServletException
|
||||
{
|
||||
AuthenticationFilter filter =
|
||||
createAuthenticationFilter(new DemoWebTokenGenerator("trillian", "sec"));
|
||||
public void testDoFilterWithAuthenticationFailed() throws IOException, ServletException {
|
||||
AuthenticationFilter filter = createAuthenticationFilter(new DemoWebTokenGenerator("trillian", "sec"));
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED,
|
||||
"Authorization Required");
|
||||
|
||||
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Test
|
||||
public void testDoFilterWithAuthenticationSuccess()
|
||||
throws IOException, ServletException
|
||||
{
|
||||
AuthenticationFilter filter =
|
||||
createAuthenticationFilter(new DemoWebTokenGenerator("trillian",
|
||||
"secret"));
|
||||
public void testDoFilterWithAuthenticationSuccess() throws IOException, ServletException {
|
||||
AuthenticationFilter filter = createAuthenticationFilter();
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
verify(chain).doFilter(any(HttpServletRequest.class),
|
||||
any(HttpServletResponse.class));
|
||||
|
||||
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
|
||||
}
|
||||
|
||||
//~--- set methods ----------------------------------------------------------
|
||||
@Test
|
||||
public void testExpiredBearerToken() throws IOException, ServletException {
|
||||
WebTokenGenerator generator = mock(WebTokenGenerator.class);
|
||||
when(generator.createToken(request)).thenReturn(BearerToken.create(null,
|
||||
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzY21hZG1pbiIsImp0aSI6IjNqUzZ4TzMwUzEiLCJpYXQiOjE1OTY3ODA5Mjg"
|
||||
+ "sImV4cCI6MTU5Njc0NDUyOCwic2NtLW1hbmFnZXIucmVmcmVzaEV4cGlyYXRpb24iOjE1OTY4MjQxMjg2MDIsInNjbS1tYW5h"
|
||||
+ "Z2VyLnBhcmVudFRva2VuSWQiOiIzalM2eE8zMFMxIn0.utZLmzGZr-M6MP19yrd0dgLPkJ0u1xojwHKQi36_QAs"));
|
||||
AuthenticationFilter filter = createAuthenticationFilter(generator);
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
public void setUp() {
|
||||
configuration = new ScmConfiguration();
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param generators
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private AuthenticationFilter createAuthenticationFilter(
|
||||
WebTokenGenerator... generators)
|
||||
{
|
||||
return new AuthenticationFilter(configuration,
|
||||
ImmutableSet.copyOf(generators));
|
||||
private AuthenticationFilter createAuthenticationFilter(WebTokenGenerator... generators) {
|
||||
return new AuthenticationFilter(configuration, ImmutableSet.copyOf(generators));
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
private static class DemoWebTokenGenerator implements WebTokenGenerator {
|
||||
|
||||
/**
|
||||
* Class description
|
||||
*
|
||||
*
|
||||
* @version Enter version here..., 15/02/21
|
||||
* @author Enter your name here...
|
||||
*/
|
||||
private static class DemoWebTokenGenerator implements WebTokenGenerator
|
||||
{
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
*/
|
||||
public DemoWebTokenGenerator(String username, String password)
|
||||
{
|
||||
public DemoWebTokenGenerator(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
//~--- methods ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken createToken(HttpServletRequest request)
|
||||
{
|
||||
public AuthenticationToken createToken(HttpServletRequest request) {
|
||||
return new UsernamePasswordToken(username, password);
|
||||
}
|
||||
|
||||
//~--- fields -------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final String password;
|
||||
|
||||
/** Field description */
|
||||
private final String username;
|
||||
}
|
||||
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
/** Field description */
|
||||
@Mock
|
||||
private FilterChain chain;
|
||||
|
||||
/** Field description */
|
||||
private ScmConfiguration configuration;
|
||||
|
||||
/** Field description */
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
/** Field description */
|
||||
@Mock
|
||||
private HttpServletResponse response;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.web.filter;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sonia.scm.web.filter.JwtValidator.isJwtTokenExpired;
|
||||
|
||||
class JwtValidatorTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnFalseIfNotJwtToken() {
|
||||
String raw = "scmadmin.scmadmin.scmadmin";
|
||||
|
||||
boolean result = isJwtTokenExpired(raw);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateExpiredJwtToken() {
|
||||
String raw = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzY21hZG1pbiIsImp0aSI6IjNqUzZ4TzMwUzEiLCJpYXQiOjE1OTY3ODA5Mjgs"
|
||||
+ "ImV4cCI6MTU5Njc0NDUyOCwic2NtLW1hbmFnZXIucmVmcmVzaEV4cGlyYXRpb24iOjE1OTY4MjQxMjg2MDIsInNjbS1tYW5hZ2VyLnB"
|
||||
+ "hcmVudFRva2VuSWQiOiIzalM2eE8zMFMxIn0.utZLmzGZr-M6MP19yrd0dgLPkJ0u1xojwHKQi36_QAs";
|
||||
|
||||
boolean result = isJwtTokenExpired(raw);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateNotExpiredJwtToken() {
|
||||
String raw = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzY21hZG1pbiIsImp0aSI6IjNqUzZ4TzMwUzEiLCJpYXQiOjE1OTY3ODA5Mjgs"
|
||||
+ "ImV4cCI6NTU5Njc0NDUyOCwic2NtLW1hbmFnZXIucmVmcmVzaEV4cGlyYXRpb24iOjE1OTY4MjQxMjg2MDIsInNjbS1tYW5hZ2VyLnB"
|
||||
+ "hcmVudFRva2VuSWQiOiIzalM2eE8zMFMxIn0.cvK4E58734T2PqtEqqhYCInnX_uryUkMhRNX-94riY0";
|
||||
|
||||
boolean result = isJwtTokenExpired(raw);
|
||||
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,8 +60,8 @@ class RepositoryRoles extends React.Component<Props> {
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps: Props) => {
|
||||
const { loading, list, page, rolesLink, location, fetchRolesByPage } = this.props;
|
||||
if (list && page && !loading) {
|
||||
const { loading, error, list, page, rolesLink, location, fetchRolesByPage } = this.props;
|
||||
if (list && page && !loading && !error) {
|
||||
const statePage: number = list.page + 1;
|
||||
if (page !== statePage || prevProps.location.search !== location.search) {
|
||||
fetchRolesByPage(rolesLink, page);
|
||||
@@ -73,7 +73,7 @@ class RepositoryRoles extends React.Component<Props> {
|
||||
const { t, loading, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification />;
|
||||
return <ErrorNotification error={error}/>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
|
||||
@@ -73,7 +73,7 @@ class App extends Component<Props> {
|
||||
let content;
|
||||
const navigation = authenticated ? <PrimaryNavigation links={links} /> : "";
|
||||
|
||||
if (!authenticated) {
|
||||
if (!authenticated && !loading) {
|
||||
content = <Login />;
|
||||
} else if (loading) {
|
||||
content = <Loading />;
|
||||
|
||||
@@ -25,7 +25,6 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { History } from "history";
|
||||
import { Group, PagedCollection } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CreateButton,
|
||||
@@ -34,7 +33,8 @@ import {
|
||||
OverviewPageActions,
|
||||
Page,
|
||||
PageActions,
|
||||
urls
|
||||
urls,
|
||||
Loading
|
||||
} from "@scm-manager/ui-components";
|
||||
import { getGroupsLink } from "../../modules/indexResource";
|
||||
import {
|
||||
@@ -88,6 +88,11 @@ class Groups extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { groups, loading, error, canAddGroups, t } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page title={t("groups.title")} subtitle={t("groups.subtitle")} loading={loading || !groups} error={error}>
|
||||
{this.renderGroupTable()}
|
||||
|
||||
@@ -33,7 +33,8 @@ import {
|
||||
OverviewPageActions,
|
||||
Page,
|
||||
PageActions,
|
||||
urls
|
||||
urls,
|
||||
Loading
|
||||
} from "@scm-manager/ui-components";
|
||||
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||
import {
|
||||
@@ -80,6 +81,11 @@ class Overview extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { error, loading, showCreateButton, t } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page title={t("overview.title")} subtitle={t("overview.subtitle")} loading={loading} error={error}>
|
||||
{this.renderOverview()}
|
||||
|
||||
@@ -25,7 +25,6 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { History } from "history";
|
||||
import { PagedCollection, User } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CreateButton,
|
||||
@@ -34,7 +33,8 @@ import {
|
||||
OverviewPageActions,
|
||||
Page,
|
||||
PageActions,
|
||||
urls
|
||||
urls,
|
||||
Loading
|
||||
} from "@scm-manager/ui-components";
|
||||
import { getUsersLink } from "../../modules/indexResource";
|
||||
import {
|
||||
@@ -88,6 +88,11 @@ class Users extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { users, loading, error, canAddUsers, t } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page title={t("users.title")} subtitle={t("users.subtitle")} loading={loading || !users} error={error}>
|
||||
{this.renderUserTable()}
|
||||
|
||||
Reference in New Issue
Block a user