mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 06:25:45 +01:00
Merge with default
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -450,7 +450,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>sonia.scm.maven</groupId>
|
<groupId>sonia.scm.maven</groupId>
|
||||||
<artifactId>smp-maven-plugin</artifactId>
|
<artifactId>smp-maven-plugin</artifactId>
|
||||||
<version>1.0.0-alpha-8</version>
|
<version>1.0.0-rc1</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import com.google.common.base.Preconditions;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import org.apache.shiro.authc.AuthenticationToken;
|
import org.apache.shiro.authc.AuthenticationToken;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token used for authentication with bearer tokens.
|
* Token used for authentication with bearer tokens.
|
||||||
*
|
*
|
||||||
@@ -45,14 +47,17 @@ import org.apache.shiro.authc.AuthenticationToken;
|
|||||||
*/
|
*/
|
||||||
public final class BearerToken implements AuthenticationToken {
|
public final class BearerToken implements AuthenticationToken {
|
||||||
|
|
||||||
|
private final SessionId sessionId;
|
||||||
private final String raw;
|
private final String raw;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new instance.
|
* Constructs a new instance.
|
||||||
*
|
*
|
||||||
|
* @param sessionId session id of the client
|
||||||
* @param raw raw bearer token
|
* @param raw raw bearer token
|
||||||
*/
|
*/
|
||||||
private BearerToken(String raw) {
|
private BearerToken(SessionId sessionId, String raw) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
this.raw = raw;
|
this.raw = raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +72,13 @@ public final class BearerToken implements AuthenticationToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns always {@code null}.
|
* Returns the session id or {@code null}.
|
||||||
*
|
*
|
||||||
* @return {@code null}
|
* @return session id or {@code null}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Object getPrincipal() {
|
public SessionId getPrincipal() {
|
||||||
return null;
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,6 +90,23 @@ public final class BearerToken implements AuthenticationToken {
|
|||||||
*/
|
*/
|
||||||
public static BearerToken valueOf(String raw){
|
public static BearerToken valueOf(String raw){
|
||||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(raw), "raw token is required");
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(raw), "raw token is required");
|
||||||
return new BearerToken(raw);
|
return new BearerToken(null, raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link BearerToken} from raw string representation for the given ui session id.
|
||||||
|
*
|
||||||
|
* @param sessionId session id of the client
|
||||||
|
* @param rawToken bearer token string representation
|
||||||
|
*
|
||||||
|
* @return new bearer token
|
||||||
|
*/
|
||||||
|
public static BearerToken create(@Nullable String sessionId, String rawToken) {
|
||||||
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(rawToken), "raw token is required");
|
||||||
|
SessionId session = null;
|
||||||
|
if (!Strings.isNullOrEmpty(sessionId)) {
|
||||||
|
session = SessionId.valueOf(sessionId);
|
||||||
|
}
|
||||||
|
return new BearerToken(session, rawToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public final class DAORealmHelper {
|
|||||||
UsernamePasswordToken upt = (UsernamePasswordToken) token;
|
UsernamePasswordToken upt = (UsernamePasswordToken) token;
|
||||||
String principal = upt.getUsername();
|
String principal = upt.getUsername();
|
||||||
|
|
||||||
return getAuthenticationInfo(principal, null, null);
|
return getAuthenticationInfo(principal, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,8 +129,9 @@ public final class DAORealmHelper {
|
|||||||
return new AuthenticationInfoBuilder(principal);
|
return new AuthenticationInfoBuilder(principal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SimpleAuthenticationInfo getAuthenticationInfo(
|
||||||
private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) {
|
String principal, String credentials, Scope scope, SessionId sessionId
|
||||||
|
) {
|
||||||
checkArgument(!Strings.isNullOrEmpty(principal), "username is required");
|
checkArgument(!Strings.isNullOrEmpty(principal), "username is required");
|
||||||
|
|
||||||
LOG.debug("try to authenticate {}", principal);
|
LOG.debug("try to authenticate {}", principal);
|
||||||
@@ -150,6 +151,10 @@ public final class DAORealmHelper {
|
|||||||
collection.add(user, realm);
|
collection.add(user, realm);
|
||||||
collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm);
|
collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm);
|
||||||
|
|
||||||
|
if (sessionId != null) {
|
||||||
|
collection.add(sessionId, realm);
|
||||||
|
}
|
||||||
|
|
||||||
String creds = credentials;
|
String creds = credentials;
|
||||||
|
|
||||||
if (credentials == null) {
|
if (credentials == null) {
|
||||||
@@ -170,7 +175,7 @@ public final class DAORealmHelper {
|
|||||||
|
|
||||||
private String credentials;
|
private String credentials;
|
||||||
private Scope scope;
|
private Scope scope;
|
||||||
private Iterable<String> groups = Collections.emptySet();
|
private SessionId sessionId;
|
||||||
|
|
||||||
private AuthenticationInfoBuilder(String principal) {
|
private AuthenticationInfoBuilder(String principal) {
|
||||||
this.principal = principal;
|
this.principal = principal;
|
||||||
@@ -201,17 +206,17 @@ public final class DAORealmHelper {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info.
|
* With the session id.
|
||||||
// *
|
*
|
||||||
// * @param groups extra groups
|
* @param sessionId session id
|
||||||
// *
|
*
|
||||||
// * @return {@code this}
|
* @return {@code this}
|
||||||
// */
|
*/
|
||||||
// public AuthenticationInfoBuilder withGroups(Iterable<String> groups) {
|
public AuthenticationInfoBuilder withSessionId(SessionId sessionId) {
|
||||||
// this.groups = groups;
|
this.sessionId = sessionId;
|
||||||
// return this;
|
return this;
|
||||||
// }
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build creates the authentication info from the given information.
|
* Build creates the authentication info from the given information.
|
||||||
@@ -219,7 +224,7 @@ public final class DAORealmHelper {
|
|||||||
* @return authentication info
|
* @return authentication info
|
||||||
*/
|
*/
|
||||||
public AuthenticationInfo build() {
|
public AuthenticationInfo build() {
|
||||||
return getAuthenticationInfo(principal, credentials, scope);
|
return getAuthenticationInfo(principal, credentials, scope, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
41
scm-core/src/main/java/sonia/scm/security/SessionId.java
Normal file
41
scm-core/src/main/java/sonia/scm/security/SessionId.java
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client side session id.
|
||||||
|
*/
|
||||||
|
public final class SessionId {
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
private SessionId(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
SessionId sessionID = (SessionId) o;
|
||||||
|
return Objects.equals(value, sessionID.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SessionId valueOf(String value) {
|
||||||
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(value), "session id could not be empty or null");
|
||||||
|
return new SessionId(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,6 +116,12 @@ public final class HttpUtil
|
|||||||
*/
|
*/
|
||||||
public static final String HEADER_SCM_CLIENT = "X-SCM-Client";
|
public static final String HEADER_SCM_CLIENT = "X-SCM-Client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* header for identifying the scm-manager client session
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public static final String HEADER_SCM_SESSION = "X-SCM-Session-ID";
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
public static final String HEADER_USERAGENT = "User-Agent";
|
public static final String HEADER_USERAGENT = "User-Agent";
|
||||||
|
|
||||||
@@ -698,8 +704,10 @@ public final class HttpUtil
|
|||||||
String defaultValue)
|
String defaultValue)
|
||||||
{
|
{
|
||||||
String value = request.getHeader(header);
|
String value = request.getHeader(header);
|
||||||
|
if (value == null) {
|
||||||
return MoreObjects.firstNonNull(value, defaultValue);
|
value = defaultValue;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ class DAORealmHelperTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private UserDAO userDAO;
|
private UserDAO userDAO;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private GroupDAO groupDAO;
|
|
||||||
|
|
||||||
private DAORealmHelper helper;
|
private DAORealmHelper helper;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -87,6 +84,21 @@ class DAORealmHelperTest {
|
|||||||
assertThat(principals.oneByType(Scope.class)).isSameAs(scope);
|
assertThat(principals.oneByType(Scope.class)).isSameAs(scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnAuthenticationInfoWithSessionId() {
|
||||||
|
User user = new User("trillian");
|
||||||
|
when(userDAO.get("trillian")).thenReturn(user);
|
||||||
|
|
||||||
|
SessionId session = SessionId.valueOf("abc123");
|
||||||
|
|
||||||
|
AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian")
|
||||||
|
.withSessionId(session)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
PrincipalCollection principals = authenticationInfo.getPrincipals();
|
||||||
|
assertThat(principals.oneByType(SessionId.class)).isSameAs(session);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnAuthenticationInfoWithCredentials() {
|
void shouldReturnAuthenticationInfoWithCredentials() {
|
||||||
User user = new User("trillian");
|
User user = new User("trillian");
|
||||||
|
|||||||
@@ -54,6 +54,31 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
public class HttpUtilTest
|
public class HttpUtilTest
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetHeader() {
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
when(request.getHeader("Test")).thenReturn("Value-One");
|
||||||
|
|
||||||
|
String value = HttpUtil.getHeader(request, "Test", "Fallback");
|
||||||
|
assertEquals("Value-One", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetHeaderWithDefaultValue() {
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
|
||||||
|
String value = HttpUtil.getHeader(request, "Test", "Fallback");
|
||||||
|
assertEquals("Fallback", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetHeaderWithNullAsDefaultValue() {
|
||||||
|
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||||
|
|
||||||
|
String value = HttpUtil.getHeader(request, "Test", null);
|
||||||
|
assertNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void concatenateTest() {
|
public void concatenateTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { FormEvent } from "react";
|
import React, { FormEvent } from "react";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
import { Branch, Repository, Link } from "@scm-manager/ui-types";
|
import { Branch, Repository, Link } from "@scm-manager/ui-types";
|
||||||
import { apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton } from "@scm-manager/ui-components";
|
import { apiClient, BranchSelector, ErrorPage, Loading, Subtitle, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = WithTranslation & {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -143,10 +143,14 @@ class RepositoryConfig extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submitButton = disabled ? null : (
|
const submitButton = disabled ? null : (
|
||||||
<SubmitButton
|
<Level
|
||||||
label={t("scm-git-plugin.repo-config.submit")}
|
right={
|
||||||
loading={submitPending}
|
<SubmitButton
|
||||||
disabled={!this.state.selectedBranchName}
|
label={t("scm-git-plugin.repo-config.submit")}
|
||||||
|
loading={submitPending}
|
||||||
|
disabled={!this.state.selectedBranchName}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
<systemProperties>
|
<systemProperties>
|
||||||
<arg>java.awt.headless=true</arg>
|
<arg>java.awt.headless=true</arg>
|
||||||
<arg>logback.configurationFile=logging.xml</arg>
|
<arg>logback.configurationFile=logging.xml</arg>
|
||||||
|
<arg>ClassLoaderLeakPreventor.threadWaitMs=100</arg>
|
||||||
</systemProperties>
|
</systemProperties>
|
||||||
</jvmSettings>
|
</jvmSettings>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<build.script>build</build.script>
|
<build.script>build</build.script>
|
||||||
|
<skipTests>false</skipTests>
|
||||||
<sonar.language>typescript</sonar.language>
|
<sonar.language>typescript</sonar.language>
|
||||||
<sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources>
|
<sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources>
|
||||||
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
|
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"@scm-manager/ui-types": "^2.0.0-SNAPSHOT",
|
"@scm-manager/ui-types": "^2.0.0-SNAPSHOT",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"date-fns": "^2.4.1",
|
"date-fns": "^2.4.1",
|
||||||
|
"event-source-polyfill": "^1.0.9",
|
||||||
"query-string": "5",
|
"query-string": "5",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-diff-view": "^1.8.1",
|
"react-diff-view": "^1.8.1",
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Forms|Checkbox Default 1`] = `
|
exports[`Storyshots Forms|Checkbox Default 1`] = `
|
||||||
<div
|
<div
|
||||||
className="sc-fBuWsC ldmpJA"
|
className="sc-caSCKo brLbbv"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="field"
|
className="field"
|
||||||
@@ -381,7 +381,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
||||||
<div
|
<div
|
||||||
className="sc-fBuWsC ldmpJA"
|
className="sc-caSCKo brLbbv"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="field"
|
className="field"
|
||||||
@@ -409,7 +409,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Forms|Radio Default 1`] = `
|
exports[`Storyshots Forms|Radio Default 1`] = `
|
||||||
<div
|
<div
|
||||||
className="sc-fMiknA keSQNk"
|
className="sc-gisBJw jHakbY"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="radio"
|
className="radio"
|
||||||
@@ -438,7 +438,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Forms|Radio Disabled 1`] = `
|
exports[`Storyshots Forms|Radio Disabled 1`] = `
|
||||||
<div
|
<div
|
||||||
className="sc-fMiknA keSQNk"
|
className="sc-gisBJw jHakbY"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="radio"
|
className="radio"
|
||||||
@@ -456,6 +456,83 @@ exports[`Storyshots Forms|Radio Disabled 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Forms|Textarea OnCancel 1`] = `
|
||||||
|
<div
|
||||||
|
className="sc-kjoXOD hVPZau"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="field"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
disabled={false}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
value="Use the escape key to clear the textarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Forms|Textarea OnChange 1`] = `
|
||||||
|
<div
|
||||||
|
className="sc-kjoXOD hVPZau"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="field"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
disabled={false}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
value="Start typing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
Start typing
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Forms|Textarea OnSubmit 1`] = `
|
||||||
|
<div
|
||||||
|
className="sc-kjoXOD hVPZau"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="field"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
disabled={false}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
value="Use the ctrl/command + Enter to submit the textarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Storyshots Loading Default 1`] = `
|
exports[`Storyshots Loading Default 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -2481,3 +2558,33 @@ exports[`Storyshots Table|Table TextColumn 1`] = `
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Toast Click to close 1`] = `null`;
|
||||||
|
|
||||||
|
exports[`Storyshots Toast Danger 1`] = `null`;
|
||||||
|
|
||||||
|
exports[`Storyshots Toast Info 1`] = `null`;
|
||||||
|
|
||||||
|
exports[`Storyshots Toast Open/Close 1`] = `
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"padding": "2rem",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="button is-primary"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
Toast
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Toast Primary 1`] = `null`;
|
||||||
|
|
||||||
|
exports[`Storyshots Toast Success 1`] = `null`;
|
||||||
|
|
||||||
|
exports[`Storyshots Toast Warning 1`] = `null`;
|
||||||
|
|||||||
@@ -1,6 +1,43 @@
|
|||||||
import { contextPath } from "./urls";
|
import { contextPath } from "./urls";
|
||||||
import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors";
|
// @ts-ignore we have not types for event-source-polyfill
|
||||||
import { BackendErrorContent } from "./errors";
|
import { EventSourcePolyfill } from "event-source-polyfill";
|
||||||
|
import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError, BackendErrorContent } from "./errors";
|
||||||
|
|
||||||
|
type SubscriptionEvent = {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OpenEvent = SubscriptionEvent;
|
||||||
|
|
||||||
|
type ErrorEvent = SubscriptionEvent & {
|
||||||
|
error: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MessageEvent = SubscriptionEvent & {
|
||||||
|
data: string;
|
||||||
|
lastEventId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MessageListeners = {
|
||||||
|
[eventType: string]: (event: MessageEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscriptionContext = {
|
||||||
|
onOpen?: OpenEvent;
|
||||||
|
onMessage: MessageListeners;
|
||||||
|
onError?: ErrorEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscriptionArgument = MessageListeners | SubscriptionContext;
|
||||||
|
|
||||||
|
type Cancel = () => void;
|
||||||
|
|
||||||
|
const sessionId = (
|
||||||
|
Date.now().toString(36) +
|
||||||
|
Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 5)
|
||||||
|
).toUpperCase();
|
||||||
|
|
||||||
const extractXsrfTokenFromJwt = (jwt: string) => {
|
const extractXsrfTokenFromJwt = (jwt: string) => {
|
||||||
const parts = jwt.split(".");
|
const parts = jwt.split(".");
|
||||||
@@ -26,26 +63,34 @@ const extractXsrfToken = () => {
|
|||||||
return extractXsrfTokenFromCookie(document.cookie);
|
return extractXsrfTokenFromCookie(document.cookie);
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
|
const createRequestHeaders = () => {
|
||||||
if (!o.headers) {
|
const headers: { [key: string]: string } = {
|
||||||
o.headers = {};
|
// disable caching for now
|
||||||
}
|
Cache: "no-cache",
|
||||||
|
// identify the request as ajax request
|
||||||
// @ts-ignore We are sure that here we only get headers of type Record<string, string>
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
const headers: Record<string, string> = o.headers;
|
// identify the web interface
|
||||||
headers["Cache"] = "no-cache";
|
"X-SCM-Client": "WUI",
|
||||||
// identify the request as ajax request
|
// identify the window session
|
||||||
headers["X-Requested-With"] = "XMLHttpRequest";
|
"X-SCM-Session-ID": sessionId
|
||||||
// identify the web interface
|
};
|
||||||
headers["X-SCM-Client"] = "WUI";
|
|
||||||
|
|
||||||
const xsrf = extractXsrfToken();
|
const xsrf = extractXsrfToken();
|
||||||
if (xsrf) {
|
if (xsrf) {
|
||||||
headers["X-XSRF-Token"] = xsrf;
|
headers["X-XSRF-Token"] = xsrf;
|
||||||
}
|
}
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
|
||||||
|
if (o.headers) {
|
||||||
|
o.headers = {
|
||||||
|
...createRequestHeaders()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
o.headers = createRequestHeaders();
|
||||||
|
}
|
||||||
o.credentials = "same-origin";
|
o.credentials = "same-origin";
|
||||||
o.headers = headers;
|
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,12 +210,39 @@ class ApiClient {
|
|||||||
if (!options.headers) {
|
if (!options.headers) {
|
||||||
options.headers = {};
|
options.headers = {};
|
||||||
}
|
}
|
||||||
// @ts-ignore We are sure that here we only get headers of type Record<string, string>
|
// @ts-ignore We are sure that here we only get headers of type {[name:string]: string}
|
||||||
options.headers["Content-Type"] = contentType;
|
options.headers["Content-Type"] = contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(createUrl(url), options).then(handleFailure);
|
return fetch(createUrl(url), options).then(handleFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscribe(url: string, argument: SubscriptionArgument): Cancel {
|
||||||
|
const es = new EventSourcePolyfill(createUrl(url), {
|
||||||
|
withCredentials: true,
|
||||||
|
headers: createRequestHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let listeners: MessageListeners;
|
||||||
|
// type guard, to identify that argument is of type SubscriptionContext
|
||||||
|
if ("onMessage" in argument) {
|
||||||
|
listeners = (argument as SubscriptionContext).onMessage;
|
||||||
|
if (argument.onError) {
|
||||||
|
es.onerror = argument.onError;
|
||||||
|
}
|
||||||
|
if (argument.onOpen) {
|
||||||
|
es.onopen = argument.onOpen;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listeners = argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const type in listeners) {
|
||||||
|
es.addEventListener(type, listeners[type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => es.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = new ApiClient();
|
export const apiClient = new ApiClient();
|
||||||
|
|||||||
56
scm-ui/ui-components/src/forms/Textarea.stories.tsx
Normal file
56
scm-ui/ui-components/src/forms/Textarea.stories.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React, {useState} from "react";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Textarea from "./Textarea";
|
||||||
|
|
||||||
|
const Spacing = styled.div`
|
||||||
|
padding: 2em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OnChangeTextarea = () => {
|
||||||
|
const [value, setValue] = useState("Start typing");
|
||||||
|
return (
|
||||||
|
<Spacing>
|
||||||
|
<Textarea value={value} onChange={v => setValue(v)} />
|
||||||
|
<hr />
|
||||||
|
<p>{value}</p>
|
||||||
|
</Spacing>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OnSubmitTextare = () => {
|
||||||
|
const [value, setValue] = useState("Use the ctrl/command + Enter to submit the textarea");
|
||||||
|
const [submitted, setSubmitted] = useState("");
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
setSubmitted(value);
|
||||||
|
setValue("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spacing>
|
||||||
|
<Textarea value={value} onChange={v => setValue(v)} onSubmit={submit} />
|
||||||
|
<hr />
|
||||||
|
<p>{submitted}</p>
|
||||||
|
</Spacing>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OnCancelTextare = () => {
|
||||||
|
const [value, setValue] = useState("Use the escape key to clear the textarea");
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
setValue("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spacing>
|
||||||
|
<Textarea value={value} onChange={v => setValue(v)} onCancel={cancel} />
|
||||||
|
</Spacing>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
storiesOf("Forms|Textarea", module)
|
||||||
|
.add("OnChange", () => <OnChangeTextarea />)
|
||||||
|
.add("OnSubmit", () => <OnSubmitTextare />)
|
||||||
|
.add("OnCancel", () => <OnCancelTextare />);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { ChangeEvent } from "react";
|
import React, { ChangeEvent, KeyboardEvent } from "react";
|
||||||
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -10,6 +10,8 @@ type Props = {
|
|||||||
onChange: (value: string, name?: string) => void;
|
onChange: (value: string, name?: string) => void;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onSubmit?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Textarea extends React.Component<Props> {
|
class Textarea extends React.Component<Props> {
|
||||||
@@ -25,6 +27,19 @@ class Textarea extends React.Component<Props> {
|
|||||||
this.props.onChange(event.target.value, this.props.name);
|
this.props.onChange(event.target.value, this.props.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
const { onCancel } = this.props;
|
||||||
|
if (onCancel && event.key === "Escape") {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onSubmit } = this.props;
|
||||||
|
if (onSubmit && event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { placeholder, value, label, helpText, disabled } = this.props;
|
const { placeholder, value, label, helpText, disabled } = this.props;
|
||||||
|
|
||||||
@@ -41,6 +56,7 @@ class Textarea extends React.Component<Props> {
|
|||||||
onChange={this.handleInput}
|
onChange={this.handleInput}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={!!disabled}
|
disabled={!!disabled}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export * from "./modals";
|
|||||||
export * from "./navigation";
|
export * from "./navigation";
|
||||||
export * from "./repos";
|
export * from "./repos";
|
||||||
export * from "./table";
|
export * from "./table";
|
||||||
|
export * from "./toast";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
File,
|
File,
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Subtitle extends React.Component<Props> {
|
class Subtitle extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { subtitle } = this.props;
|
const { subtitle, className } = this.props;
|
||||||
if (subtitle) {
|
if (subtitle) {
|
||||||
return <h2 className="subtitle">{subtitle}</h2>;
|
return <h2 className={classNames("subtitle", className)}>{subtitle}</h2>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
import React from "react";
|
import React, { FC, useEffect } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
customPageTitle?: string;
|
||||||
|
preventRefreshingPageTitle?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Title extends React.Component<Props> {
|
const Title: FC<Props> = ({ title, preventRefreshingPageTitle, customPageTitle, className }) => {
|
||||||
render() {
|
useEffect(() => {
|
||||||
const { title } = this.props;
|
if (!preventRefreshingPageTitle) {
|
||||||
if (title) {
|
if (customPageTitle) {
|
||||||
return <h1 className="title">{title}</h1>;
|
document.title = customPageTitle;
|
||||||
|
} else if (title) {
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
},[title, preventRefreshingPageTitle, customPageTitle]);
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
return <h1 className={classNames("title", className)}>{title}</h1>;
|
||||||
}
|
}
|
||||||
}
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Title.defaultProps = {
|
||||||
|
preventRefreshingPageTitle: false
|
||||||
|
};
|
||||||
|
|
||||||
export default Title;
|
export default Title;
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ const Table: FC<Props> = ({ data, sortable, children, emptyMessage }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDataToColumns = (row: any) => {
|
const mapDataToColumns = (row: any, rowIndex: number) => {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr key={rowIndex}>
|
||||||
{React.Children.map(children, (child, columnIndex) => {
|
{React.Children.map(children, (child, columnIndex) => {
|
||||||
return <td>{React.cloneElement(child, { ...child.props, columnIndex, row })}</td>;
|
return <td>{React.cloneElement(child, { ...child.props, columnIndex, row })}</td>;
|
||||||
})}
|
})}
|
||||||
@@ -93,6 +93,7 @@ const Table: FC<Props> = ({ data, sortable, children, emptyMessage }) => {
|
|||||||
onClick={isSortable(child) ? () => tableSort(index) : undefined}
|
onClick={isSortable(child) ? () => tableSort(index) : undefined}
|
||||||
onMouseEnter={() => setHoveredColumnIndex(index)}
|
onMouseEnter={() => setHoveredColumnIndex(index)}
|
||||||
onMouseLeave={() => setHoveredColumnIndex(undefined)}
|
onMouseLeave={() => setHoveredColumnIndex(undefined)}
|
||||||
|
key={index}
|
||||||
>
|
>
|
||||||
{child.props.header}
|
{child.props.header}
|
||||||
{isSortable(child) && renderSortIcon(child, ascending, shouldShowIcon(index))}
|
{isSortable(child) && renderSortIcon(child, ascending, shouldShowIcon(index))}
|
||||||
|
|||||||
62
scm-ui/ui-components/src/toast/Toast.tsx
Normal file
62
scm-ui/ui-components/src/toast/Toast.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { getTheme, Themeable, ToastThemeContext, Type } from "./themes";
|
||||||
|
import usePortalRootElement from "../usePortalRootElement";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: Type;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Container = styled.div<Themeable>`
|
||||||
|
z-index: 99999;
|
||||||
|
position: fixed;
|
||||||
|
padding: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
color: ${props => props.theme.primary};
|
||||||
|
background-color: ${props => props.theme.secondary};
|
||||||
|
max-width: 18rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
animation: 0.5s slide-up;
|
||||||
|
|
||||||
|
& > p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
bottom: -10rem;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled.h1<Themeable>`
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Toast: FC<Props> = ({ children, title, type }) => {
|
||||||
|
const rootElement = usePortalRootElement("toastRoot");
|
||||||
|
if (!rootElement) {
|
||||||
|
// portal not yet ready
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = getTheme(type);
|
||||||
|
const content = (
|
||||||
|
<Container theme={theme}>
|
||||||
|
<Title theme={theme}>{title}</Title>
|
||||||
|
<ToastThemeContext.Provider value={theme}>{children}</ToastThemeContext.Provider>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
return createPortal(content, rootElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toast;
|
||||||
46
scm-ui/ui-components/src/toast/ToastButton.tsx
Normal file
46
scm-ui/ui-components/src/toast/ToastButton.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { FC, useContext, MouseEvent } from "react";
|
||||||
|
import { ToastThemeContext, Themeable } from "./themes";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
icon?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemedButton = styled.button.attrs(props => ({
|
||||||
|
className: "button"
|
||||||
|
}))<Themeable>`
|
||||||
|
color: ${props => props.theme.primary};
|
||||||
|
border-color: ${props => props.theme.primary};
|
||||||
|
background-color: ${props => props.theme.secondary};
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${props => props.theme.primary};
|
||||||
|
border-color: ${props => props.theme.tertiary};
|
||||||
|
background-color: ${props => props.theme.tertiary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ToastButtonIcon = styled.i`
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ToastButton: FC<Props> = ({ icon, onClick, children }) => {
|
||||||
|
const theme = useContext(ToastThemeContext);
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (onClick) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemedButton theme={theme} onClick={handleClick}>
|
||||||
|
{icon && <ToastButtonIcon className={`fas fa-fw fa-${icon}`} />} {children}
|
||||||
|
</ThemedButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToastButton;
|
||||||
20
scm-ui/ui-components/src/toast/ToastButtons.tsx
Normal file
20
scm-ui/ui-components/src/toast/ToastButtons.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Buttons = styled.div`
|
||||||
|
display: flex;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > *:not(:last-child) {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ToastButtons: FC = ({ children }) => <Buttons>{children}</Buttons>;
|
||||||
|
|
||||||
|
export default ToastButtons;
|
||||||
66
scm-ui/ui-components/src/toast/index.stories.tsx
Normal file
66
scm-ui/ui-components/src/toast/index.stories.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import Toast from "./Toast";
|
||||||
|
import ToastButtons from "./ToastButtons";
|
||||||
|
import ToastButton from "./ToastButton";
|
||||||
|
import { types } from "./themes";
|
||||||
|
|
||||||
|
const toastStories = storiesOf("Toast", module);
|
||||||
|
|
||||||
|
const AnimatedToast = () => (
|
||||||
|
<Toast type="primary" title="Animated">
|
||||||
|
Awesome animated Toast
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Animator = () => {
|
||||||
|
const [display, setDisplay] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "2rem" }}>
|
||||||
|
{display && <AnimatedToast />}
|
||||||
|
<button className="button is-primary" onClick={() => setDisplay(!display)}>
|
||||||
|
{display ? "Close" : "Open"} Toast
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Closeable = () => {
|
||||||
|
const [show, setShow] = useState(true);
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
setShow(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toast type="success" title="Awesome feature">
|
||||||
|
<p>Close the message with a click</p>
|
||||||
|
<ToastButtons>
|
||||||
|
<ToastButton icon="times" onClick={hide}>
|
||||||
|
Click to close
|
||||||
|
</ToastButton>
|
||||||
|
</ToastButtons>
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
toastStories.add("Open/Close", () => <Animator />);
|
||||||
|
toastStories.add("Click to close", () => <Closeable />);
|
||||||
|
|
||||||
|
types.forEach(type => {
|
||||||
|
toastStories.add(type.charAt(0).toUpperCase() + type.slice(1), () => (
|
||||||
|
<Toast type={type} title="New Changes">
|
||||||
|
<p>The underlying Pull-Request has changed. Press reload to see the changes.</p>
|
||||||
|
<p>Warning: Non saved modification will be lost.</p>
|
||||||
|
<ToastButtons>
|
||||||
|
<ToastButton icon="redo">Reload</ToastButton>
|
||||||
|
<ToastButton icon="times">Ignore</ToastButton>
|
||||||
|
</ToastButtons>
|
||||||
|
</Toast>
|
||||||
|
));
|
||||||
|
});
|
||||||
3
scm-ui/ui-components/src/toast/index.ts
Normal file
3
scm-ui/ui-components/src/toast/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as Toast } from "./Toast";
|
||||||
|
export { default as ToastButton } from "./ToastButton";
|
||||||
|
export { default as ToastButtons } from "./ToastButtons";
|
||||||
53
scm-ui/ui-components/src/toast/themes.ts
Normal file
53
scm-ui/ui-components/src/toast/themes.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export type ToastTheme = {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
tertiary: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Themeable = {
|
||||||
|
theme: ToastTheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Type = "info" | "primary" | "success" | "warning" | "danger";
|
||||||
|
|
||||||
|
export const types: Type[] = ["info", "primary", "success", "warning", "danger"];
|
||||||
|
|
||||||
|
const themes: { [name in Type]: ToastTheme } = {
|
||||||
|
info: {
|
||||||
|
primary: "#363636",
|
||||||
|
secondary: "#99d8f3",
|
||||||
|
tertiary: "white"
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
primary: "#363636",
|
||||||
|
secondary: "#7fe8ef",
|
||||||
|
tertiary: "white"
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
primary: "#363636",
|
||||||
|
secondary: "#7fe3cd",
|
||||||
|
tertiary: "white"
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
primary: "#905515",
|
||||||
|
secondary: "#ffeeab",
|
||||||
|
tertiary: "white"
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
primary: "#363636",
|
||||||
|
secondary: "#ff9baf",
|
||||||
|
tertiary: "white"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTheme = (name: Type) => {
|
||||||
|
const theme = themes[name];
|
||||||
|
if (!theme) {
|
||||||
|
throw new Error(`could not find theme with name ${name}`);
|
||||||
|
}
|
||||||
|
return theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToastThemeContext = React.createContext(themes.warning);
|
||||||
33
scm-ui/ui-components/src/usePortalRootElement.ts
Normal file
33
scm-ui/ui-components/src/usePortalRootElement.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const createElement = (id: string) => {
|
||||||
|
const element = document.createElement("div");
|
||||||
|
element.setAttribute("id", id);
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendRootElement = (rootElement: HTMLElement) => {
|
||||||
|
document.body.appendChild(rootElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePortalRootElement = (id: string) => {
|
||||||
|
const [rootElement, setRootElement] = useState<HTMLElement>();
|
||||||
|
useEffect(() => {
|
||||||
|
let element = document.getElementById(id);
|
||||||
|
if (!element) {
|
||||||
|
element = createElement(id);
|
||||||
|
appendRootElement(element);
|
||||||
|
}
|
||||||
|
setRootElement(element);
|
||||||
|
return () => {
|
||||||
|
if (element) {
|
||||||
|
element.remove();
|
||||||
|
}
|
||||||
|
setRootElement(undefined);
|
||||||
|
};
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return rootElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePortalRootElement;
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"generalNavLink": "Generell"
|
"generalNavLink": "Generell"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
|
"title": "Administration",
|
||||||
"currentAppVersion": "Aktuelle Software-Versionsnummer",
|
"currentAppVersion": "Aktuelle Software-Versionsnummer",
|
||||||
"communityTitle": "Community Support",
|
"communityTitle": "Community Support",
|
||||||
"communityIconAlt": "Community Support Icon",
|
"communityIconAlt": "Community Support Icon",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"generalNavLink": "General"
|
"generalNavLink": "General"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
|
"title": "Administration",
|
||||||
"currentAppVersion": "Current Application Version",
|
"currentAppVersion": "Current Application Version",
|
||||||
"communityTitle": "Community Support",
|
"communityTitle": "Community Support",
|
||||||
"communityIconAlt": "Community Support Icon",
|
"communityIconAlt": "Community Support Icon",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"generalNavLink": "General"
|
"generalNavLink": "General"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
|
"title": "Administración",
|
||||||
"currentAppVersion": "Versión actual de la aplicación",
|
"currentAppVersion": "Versión actual de la aplicación",
|
||||||
"communityTitle": "Soporte de la comunidad",
|
"communityTitle": "Soporte de la comunidad",
|
||||||
"communityIconAlt": "Icono del soporte de la comunidad",
|
"communityIconAlt": "Icono del soporte de la comunidad",
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ type Props = WithTranslation & {
|
|||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NoBottomMarginSubtitle = styled(Subtitle)`
|
||||||
|
margin-bottom: 0.25rem !important;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BottomMarginDiv = styled.div`
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
const BoxShadowBox = styled.div`
|
const BoxShadowBox = styled.div`
|
||||||
box-shadow: 0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 2px rgba(40, 177, 232, 0.2);
|
box-shadow: 0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 2px rgba(40, 177, 232, 0.2);
|
||||||
`;
|
`;
|
||||||
@@ -29,8 +37,9 @@ class AdminDetails extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title title={t("admin.info.currentAppVersion")} />
|
<Title title={t("admin.info.title")} />
|
||||||
<Subtitle subtitle={this.props.version} />
|
<NoBottomMarginSubtitle subtitle={t("admin.info.currentAppVersion")} />
|
||||||
|
<BottomMarginDiv>{this.props.version}</BottomMarginDiv>
|
||||||
<BoxShadowBox className="box">
|
<BoxShadowBox className="box">
|
||||||
<article className="media">
|
<article className="media">
|
||||||
<ImageWrapper className="media-left">
|
<ImageWrapper className="media-left">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
import { Repository, Branch, BranchRequest } from "@scm-manager/ui-types";
|
import { Repository, Branch, BranchRequest } from "@scm-manager/ui-types";
|
||||||
import { Select, InputField, SubmitButton, validation as validator } from "@scm-manager/ui-components";
|
import { Select, InputField, Level, SubmitButton, validation as validator } from "@scm-manager/ui-components";
|
||||||
import { orderBranches } from "../util/orderBranches";
|
import { orderBranches } from "../util/orderBranches";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = WithTranslation & {
|
||||||
@@ -83,10 +83,14 @@ class BranchForm extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column">
|
<div className="column">
|
||||||
<SubmitButton
|
<Level
|
||||||
disabled={disabled || !this.isValid()}
|
right={
|
||||||
loading={loading}
|
<SubmitButton
|
||||||
label={t("branches.create.submit")}
|
disabled={disabled || !this.isValid()}
|
||||||
|
loading={loading}
|
||||||
|
label={t("branches.create.submit")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class FileTree extends React.Component<Props, State> {
|
|||||||
<th>{t("sources.file-tree.name")}</th>
|
<th>{t("sources.file-tree.name")}</th>
|
||||||
<th className="is-hidden-mobile">{t("sources.file-tree.length")}</th>
|
<th className="is-hidden-mobile">{t("sources.file-tree.length")}</th>
|
||||||
<th className="is-hidden-mobile">{t("sources.file-tree.lastModified")}</th>
|
<th className="is-hidden-mobile">{t("sources.file-tree.lastModified")}</th>
|
||||||
<th className="is-hidden-mobile">{t("sources.file-tree.description")}</th>
|
<th className="is-hidden-touch">{t("sources.file-tree.description")}</th>
|
||||||
{binder.hasExtension("repos.sources.tree.row.right") && <th className="is-hidden-mobile" />}
|
{binder.hasExtension("repos.sources.tree.row.right") && <th className="is-hidden-mobile" />}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const MinWidthTd = styled.td`
|
|||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const NoWrapTd = styled.td`
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
export function createLink(base: string, file: File) {
|
export function createLink(base: string, file: File) {
|
||||||
let link = base;
|
let link = base;
|
||||||
if (file.path) {
|
if (file.path) {
|
||||||
@@ -88,9 +92,9 @@ class FileTreeLeaf extends React.Component<Props> {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{this.createFileIcon(file)}</td>
|
<td>{this.createFileIcon(file)}</td>
|
||||||
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
|
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
|
||||||
<td className="is-hidden-mobile">{fileSize}</td>
|
<NoWrapTd className="is-hidden-mobile">{fileSize}</NoWrapTd>
|
||||||
<td className="is-hidden-mobile">{this.contentIfPresent(file, <DateFromNow date={file.lastModified} />)}</td>
|
<td className="is-hidden-mobile">{this.contentIfPresent(file, <DateFromNow date={file.lastModified} />)}</td>
|
||||||
<MinWidthTd className={classNames("is-word-break", "is-hidden-mobile")}>
|
<MinWidthTd className={classNames("is-word-break", "is-hidden-touch")}>
|
||||||
{this.contentIfPresent(file, file.description)}
|
{this.contentIfPresent(file, file.description)}
|
||||||
</MinWidthTd>
|
</MinWidthTd>
|
||||||
{binder.hasExtension("repos.sources.tree.row.right") && (
|
{binder.hasExtension("repos.sources.tree.row.right") && (
|
||||||
|
|||||||
@@ -580,7 +580,7 @@
|
|||||||
<jjwt.version>0.10.5</jjwt.version>
|
<jjwt.version>0.10.5</jjwt.version>
|
||||||
<selenium.version>2.53.1</selenium.version>
|
<selenium.version>2.53.1</selenium.version>
|
||||||
<wagon.version>1.0</wagon.version>
|
<wagon.version>1.0</wagon.version>
|
||||||
<mustache.version>0.8.17</mustache.version>
|
<mustache.version>0.9.6-scm1</mustache.version>
|
||||||
<netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server>
|
<netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server>
|
||||||
<sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria>
|
<sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria>
|
||||||
<sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey>
|
<sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey>
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ public class BearerRealm extends AuthenticatingRealm
|
|||||||
return helper.authenticationInfoBuilder(accessToken.getSubject())
|
return helper.authenticationInfoBuilder(accessToken.getSubject())
|
||||||
.withCredentials(bt.getCredentials())
|
.withCredentials(bt.getCredentials())
|
||||||
.withScope(Scopes.fromClaims(accessToken.getClaims()))
|
.withScope(Scopes.fromClaims(accessToken.getClaims()))
|
||||||
|
.withSessionId(bt.getPrincipal())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ public class BearerWebTokenGenerator extends SchemeBasedWebTokenGenerator
|
|||||||
|
|
||||||
if (HttpUtil.AUTHORIZATION_SCHEME_BEARER.equalsIgnoreCase(scheme))
|
if (HttpUtil.AUTHORIZATION_SCHEME_BEARER.equalsIgnoreCase(scheme))
|
||||||
{
|
{
|
||||||
token = BearerToken.valueOf(authorization);
|
String sessionId = request.getHeader(HttpUtil.HEADER_SCM_SESSION);
|
||||||
|
token = BearerToken.create(sessionId, authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import sonia.scm.util.HttpUtil;
|
import sonia.scm.util.HttpUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an {@link BearerToken} from the {@link #COOKIE_NAME}
|
* Creates an {@link BearerToken} from the {@link HttpUtil#COOKIE_BEARER_AUTHENTICATION}
|
||||||
* cookie.
|
* cookie.
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
@@ -54,7 +54,7 @@ public class CookieBearerWebTokenGenerator implements WebTokenGenerator
|
|||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an {@link BearerToken} from the {@link #COOKIE_NAME}
|
* Creates an {@link BearerToken} from the {@link HttpUtil#COOKIE_BEARER_AUTHENTICATION}
|
||||||
* cookie.
|
* cookie.
|
||||||
*
|
*
|
||||||
* @param request http servlet request
|
* @param request http servlet request
|
||||||
@@ -73,7 +73,8 @@ public class CookieBearerWebTokenGenerator implements WebTokenGenerator
|
|||||||
{
|
{
|
||||||
if (HttpUtil.COOKIE_BEARER_AUTHENTICATION.equals(cookie.getName()))
|
if (HttpUtil.COOKIE_BEARER_AUTHENTICATION.equals(cookie.getName()))
|
||||||
{
|
{
|
||||||
token = BearerToken.valueOf(cookie.getValue());
|
String sessionId = HttpUtil.getHeader(request, HttpUtil.HEADER_SCM_SESSION, null);
|
||||||
|
token = BearerToken.create(sessionId, cookie.getValue());
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,45 +177,26 @@ public class DefaultAdministrationContext implements AdministrationContext
|
|||||||
//J+
|
//J+
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void doRunAsInNonWebSessionContext(PrivilegedAction action) {
|
||||||
* Method description
|
logger.trace("bind shiro security manager to current thread");
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param action
|
|
||||||
*/
|
|
||||||
private void doRunAsInNonWebSessionContext(PrivilegedAction action)
|
|
||||||
{
|
|
||||||
if (logger.isTraceEnabled())
|
|
||||||
{
|
|
||||||
logger.trace("bind shiro security manager to current thread");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
SecurityUtils.setSecurityManager(securityManager);
|
SecurityUtils.setSecurityManager(securityManager);
|
||||||
|
|
||||||
Subject subject = createAdminSubject();
|
Subject subject = createAdminSubject();
|
||||||
ThreadState state = new SubjectThreadState(subject);
|
ThreadState state = new SubjectThreadState(subject);
|
||||||
|
|
||||||
state.bind();
|
state.bind();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (logger.isInfoEnabled())
|
logger.info("execute action {} in administration context", action.getClass().getName());
|
||||||
{
|
|
||||||
logger.info("execute action {} in administration context",
|
|
||||||
action.getClass().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
action.run();
|
action.run();
|
||||||
|
} finally {
|
||||||
|
logger.trace("restore current thread state");
|
||||||
|
state.restore();
|
||||||
}
|
}
|
||||||
finally
|
} finally {
|
||||||
{
|
|
||||||
state.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
SecurityUtils.setSecurityManager(null);
|
SecurityUtils.setSecurityManager(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,11 +84,9 @@ class BearerRealmTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDoGetAuthentication() {
|
void shouldDoGetAuthentication() {
|
||||||
BearerToken bearerToken = BearerToken.valueOf("__bearer__");
|
BearerToken bearerToken = BearerToken.create("__session__", "__bearer__");
|
||||||
AccessToken accessToken = mock(AccessToken.class);
|
AccessToken accessToken = mock(AccessToken.class);
|
||||||
|
|
||||||
Set<String> groups = ImmutableSet.of("HeartOfGold", "Puzzle42");
|
|
||||||
|
|
||||||
when(accessToken.getSubject()).thenReturn("trillian");
|
when(accessToken.getSubject()).thenReturn("trillian");
|
||||||
when(accessToken.getClaims()).thenReturn(new HashMap<>());
|
when(accessToken.getClaims()).thenReturn(new HashMap<>());
|
||||||
when(accessTokenResolver.resolve(bearerToken)).thenReturn(accessToken);
|
when(accessTokenResolver.resolve(bearerToken)).thenReturn(accessToken);
|
||||||
@@ -96,6 +94,7 @@ class BearerRealmTest {
|
|||||||
when(realmHelper.authenticationInfoBuilder("trillian")).thenReturn(builder);
|
when(realmHelper.authenticationInfoBuilder("trillian")).thenReturn(builder);
|
||||||
when(builder.withCredentials("__bearer__")).thenReturn(builder);
|
when(builder.withCredentials("__bearer__")).thenReturn(builder);
|
||||||
when(builder.withScope(any(Scope.class))).thenReturn(builder);
|
when(builder.withScope(any(Scope.class))).thenReturn(builder);
|
||||||
|
when(builder.withSessionId(any(SessionId.class))).thenReturn(builder);
|
||||||
when(builder.build()).thenReturn(authenticationInfo);
|
when(builder.build()).thenReturn(authenticationInfo);
|
||||||
|
|
||||||
AuthenticationInfo result = realm.doGetAuthenticationInfo(bearerToken);
|
AuthenticationInfo result = realm.doGetAuthenticationInfo(bearerToken);
|
||||||
|
|||||||
@@ -33,47 +33,73 @@ package sonia.scm.web;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import org.apache.shiro.authc.AuthenticationToken;
|
import org.apache.shiro.authc.AuthenticationToken;
|
||||||
import org.junit.Test;
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
import static org.hamcrest.Matchers.*;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.security.BearerToken;
|
import sonia.scm.security.BearerToken;
|
||||||
|
import sonia.scm.security.SessionId;
|
||||||
|
import sonia.scm.util.HttpUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class BearerWebTokenGeneratorTest {
|
class BearerWebTokenGeneratorTest {
|
||||||
|
|
||||||
|
private final BearerWebTokenGenerator tokenGenerator = new BearerWebTokenGenerator();
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private HttpServletRequest request;
|
private HttpServletRequest request;
|
||||||
|
|
||||||
private final BearerWebTokenGenerator tokenGenerator = new BearerWebTokenGenerator();
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateTokenWithWrongScheme()
|
void shouldNotCreateTokenWithWrongScheme() {
|
||||||
{
|
|
||||||
when(request.getHeader("Authorization")).thenReturn("BASIC ASD");
|
when(request.getHeader("Authorization")).thenReturn("BASIC ASD");
|
||||||
assertNull(tokenGenerator.createToken(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateTokenWithoutAuthorizationHeader(){
|
|
||||||
assertNull(tokenGenerator.createToken(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateToken(){
|
|
||||||
when(request.getHeader("Authorization")).thenReturn("Bearer asd");
|
|
||||||
AuthenticationToken token = tokenGenerator.createToken(request);
|
AuthenticationToken token = tokenGenerator.createToken(request);
|
||||||
assertNotNull(token);
|
|
||||||
assertThat(token, instanceOf(BearerToken.class));
|
assertThat(token).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotCreateTokenWithoutAuthorizationHeader(){
|
||||||
|
AuthenticationToken token = tokenGenerator.createToken(request);
|
||||||
|
|
||||||
|
assertThat(token).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateToken(){
|
||||||
|
when(request.getHeader("Authorization")).thenReturn("Bearer asd");
|
||||||
|
|
||||||
|
AuthenticationToken token = tokenGenerator.createToken(request);
|
||||||
|
assertThat(token)
|
||||||
|
.isNotNull()
|
||||||
|
.isInstanceOf(BearerToken.class);
|
||||||
|
|
||||||
BearerToken bt = (BearerToken) token;
|
BearerToken bt = (BearerToken) token;
|
||||||
assertThat(bt.getCredentials(), equalTo("asd"));
|
assertThat(bt.getCredentials()).isEqualTo("asd");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateTokenWithSessionId(){
|
||||||
|
doReturn("Bearer asd").when(request).getHeader("Authorization");
|
||||||
|
doReturn("bcd123").when(request).getHeader(HttpUtil.HEADER_SCM_SESSION);
|
||||||
|
|
||||||
|
AuthenticationToken token = tokenGenerator.createToken(request);
|
||||||
|
assertThat(token)
|
||||||
|
.isNotNull()
|
||||||
|
.isInstanceOf(BearerToken.class);
|
||||||
|
|
||||||
|
BearerToken bt = (BearerToken) token;
|
||||||
|
assertThat(bt.getPrincipal()).isEqualTo(SessionId.valueOf("bcd123"));
|
||||||
|
assertThat(bt.getCredentials()).isEqualTo("asd");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,82 +35,81 @@ package sonia.scm.web;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import sonia.scm.security.BearerToken;
|
import sonia.scm.security.BearerToken;
|
||||||
|
import sonia.scm.security.SessionId;
|
||||||
import static org.junit.Assert.*;
|
import sonia.scm.util.HttpUtil;
|
||||||
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import javax.servlet.http.Cookie;
|
import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import sonia.scm.util.HttpUtil;
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class CookieBearerWebTokenGeneratorTest
|
class CookieBearerWebTokenGeneratorTest {
|
||||||
{
|
|
||||||
|
private final CookieBearerWebTokenGenerator tokenGenerator = new CookieBearerWebTokenGenerator();
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateToken()
|
void shouldCreateToken() {
|
||||||
{
|
assignBearerCookie("value");
|
||||||
Cookie c = mock(Cookie.class);
|
|
||||||
|
|
||||||
when(c.getName()).thenReturn(HttpUtil.COOKIE_BEARER_AUTHENTICATION);
|
|
||||||
when(c.getValue()).thenReturn("value");
|
|
||||||
when(request.getCookies()).thenReturn(new Cookie[] { c });
|
|
||||||
|
|
||||||
BearerToken token = tokenGenerator.createToken(request);
|
BearerToken token = tokenGenerator.createToken(request);
|
||||||
|
|
||||||
assertNotNull(token);
|
assertThat(token).isNotNull();
|
||||||
assertEquals("value", token.getCredentials());
|
assertThat(token.getPrincipal()).isNull();
|
||||||
|
assertThat(token.getCredentials()).isEqualTo("value");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateTokenWithWrongCookie()
|
void shouldCreateTokenWithSessionId() {
|
||||||
{
|
when(request.getHeader(HttpUtil.HEADER_SCM_SESSION)).thenReturn("abc123");
|
||||||
|
|
||||||
|
assignBearerCookie("authc");
|
||||||
|
|
||||||
|
BearerToken token = tokenGenerator.createToken(request);
|
||||||
|
|
||||||
|
assertThat(token).isNotNull();
|
||||||
|
assertThat(token.getPrincipal()).isEqualTo(SessionId.valueOf("abc123"));
|
||||||
|
assertThat(token.getCredentials()).isEqualTo("authc");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assignBearerCookie(String value) {
|
||||||
|
assignCookie(HttpUtil.COOKIE_BEARER_AUTHENTICATION, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assignCookie(String name, String value) {
|
||||||
Cookie c = mock(Cookie.class);
|
Cookie c = mock(Cookie.class);
|
||||||
|
|
||||||
when(c.getName()).thenReturn("other-cookie");
|
when(c.getName()).thenReturn(name);
|
||||||
when(request.getCookies()).thenReturn(new Cookie[] { c });
|
lenient().when(c.getValue()).thenReturn(value);
|
||||||
assertNull(tokenGenerator.createToken(request));
|
when(request.getCookies()).thenReturn(new Cookie[]{c});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateTokenWithoutCookies()
|
void shouldNotCreateTokenForWrongCookie() {
|
||||||
{
|
assignCookie("other-cookie", "with-some-value");
|
||||||
assertNull(tokenGenerator.createToken(request));
|
|
||||||
|
BearerToken token = tokenGenerator.createToken(request);
|
||||||
|
assertThat(token).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
@Test
|
||||||
|
void shouldNotCreateTokenWithoutCookies() {
|
||||||
/** Field description */
|
BearerToken token = tokenGenerator.createToken(request);
|
||||||
private final CookieBearerWebTokenGenerator tokenGenerator =
|
assertThat(token).isNull();
|
||||||
new CookieBearerWebTokenGenerator();
|
}
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
@Mock
|
|
||||||
private HttpServletRequest request;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package sonia.scm.web.security;
|
||||||
|
|
||||||
|
import com.google.inject.Guice;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.mgt.DefaultSecurityManager;
|
||||||
|
import org.apache.shiro.mgt.SecurityManager;
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.apache.shiro.util.ThreadContext;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DefaultAdministrationContextTest {
|
||||||
|
|
||||||
|
private DefaultAdministrationContext context;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Subject subject;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void create() {
|
||||||
|
Injector injector = Guice.createInjector();
|
||||||
|
SecurityManager securityManager = new DefaultSecurityManager();
|
||||||
|
|
||||||
|
context = new DefaultAdministrationContext(injector, securityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBindSubject() {
|
||||||
|
context.runAsAdmin(() -> {
|
||||||
|
Subject adminSubject = SecurityUtils.getSubject();
|
||||||
|
assertThat(adminSubject.getPrincipal()).isEqualTo("scmsystem");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBindSubjectEvenIfAlreadyBound() {
|
||||||
|
ThreadContext.bind(subject);
|
||||||
|
try {
|
||||||
|
|
||||||
|
context.runAsAdmin(() -> {
|
||||||
|
Subject adminSubject = SecurityUtils.getSubject();
|
||||||
|
assertThat(adminSubject.getPrincipal()).isEqualTo("scmsystem");
|
||||||
|
});
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
ThreadContext.unbindSubject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRestoreCurrentSubject() {
|
||||||
|
when(subject.getPrincipal()).thenReturn("tricia");
|
||||||
|
ThreadContext.bind(subject);
|
||||||
|
try {
|
||||||
|
context.runAsAdmin(() -> {});
|
||||||
|
Subject currentSubject = SecurityUtils.getSubject();
|
||||||
|
assertThat(currentSubject.getPrincipal()).isEqualTo("tricia");
|
||||||
|
} finally {
|
||||||
|
ThreadContext.unbindSubject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user