Skip to content

Commit 24603eb

Browse files
ikhoonrainy789clavinjune
authored
Revamp the web application (#977)
This PR revamps Central Dogma's web application. Main web development stacks: - Typescript - React - Redux Toolkit - Chakra UI - Next.js UX of the legacy UI wasn't good but we didn't have the bandwidth to start to fix the problems. @rainy789 helped us to complete this mission. Many works have been done in collaboration with @rainy789. Thanks to her contribution. Co-authored-by: rainy789 <[email protected]> Co-authored-by: Clavianus Juneardo <[email protected]>
1 parent c75d820 commit 24603eb

File tree

185 files changed

+26272
-132
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

185 files changed

+26272
-132
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ typings/
132132

133133
# dotenv environment variables file
134134
.env
135+
.env*.local
135136

136137
# next.js build output
137138
.next

server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringAndCredentialServiceV1Test.java

Lines changed: 153 additions & 95 deletions
Large diffs are not rendered by default.

server/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ if (project.ext.testJavaVersion >= 16) {
7474
}
7575
}
7676

77+
if (!rootProject.hasProperty('noWeb')) {
78+
sourceSets {
79+
main {
80+
output.dir project(':webapp').file('build/javaweb'), builtBy: ':webapp:copyWeb'
81+
}
82+
}
83+
}
84+
7785
def clientRelocations = [
7886
'ace-builds/src-min-noconflict/',
7987
'angular/angular.min.js',

server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.API_V1_PATH_PREFIX;
2424
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.HEALTH_CHECK_PATH;
2525
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.METRICS_PATH;
26-
import static com.linecorp.centraldogma.server.auth.AuthProvider.BUILTIN_WEB_BASE_PATH;
2726
import static com.linecorp.centraldogma.server.auth.AuthProvider.LOGIN_API_ROUTES;
2827
import static com.linecorp.centraldogma.server.auth.AuthProvider.LOGIN_PATH;
2928
import static com.linecorp.centraldogma.server.auth.AuthProvider.LOGOUT_API_ROUTES;
@@ -126,7 +125,6 @@
126125
import com.linecorp.centraldogma.server.internal.admin.auth.CsrfTokenAuthorizer;
127126
import com.linecorp.centraldogma.server.internal.admin.auth.ExpiredSessionDeletingSessionManager;
128127
import com.linecorp.centraldogma.server.internal.admin.auth.FileBasedSessionManager;
129-
import com.linecorp.centraldogma.server.internal.admin.auth.OrElseDefaultHttpFileService;
130128
import com.linecorp.centraldogma.server.internal.admin.auth.SessionTokenAuthorizer;
131129
import com.linecorp.centraldogma.server.internal.admin.service.DefaultLogoutService;
132130
import com.linecorp.centraldogma.server.internal.admin.service.RepositoryService;
@@ -870,18 +868,37 @@ public String serviceName(ServiceRequestContext ctx) {
870868
sb.service(LOGIN_PATH, authProvider.webLoginService());
871869
// Will redirect to /web/auth/logout by default.
872870
sb.service(LOGOUT_PATH, authProvider.webLogoutService());
873-
874-
sb.serviceUnder(BUILTIN_WEB_BASE_PATH, new OrElseDefaultHttpFileService(
875-
FileService.builder(CentralDogma.class.getClassLoader(), "auth-webapp")
876-
.autoDecompress(true)
877-
.serveCompressedFiles(true)
878-
.cacheControl(ServerCacheControl.REVALIDATED)
879-
.build(),
880-
"/index.html"));
881871
}
882-
sb.serviceUnder("/",
872+
873+
// Folder names contain path patterns such as `[projectName]` which FileService can't infer from
874+
// the request path. Return `index.html` as a fallback so that Next.js client router handles the
875+
// path patterns.
876+
final HttpService fallbackFileService = HttpFile.of(CentralDogma.class.getClassLoader(),
877+
"com/linecorp/centraldogma/webapp/index.html")
878+
.asService();
879+
sb.serviceUnder("/app", FileService.builder(CentralDogma.class.getClassLoader(),
880+
"com/linecorp/centraldogma/webapp/app")
881+
.cacheControl(ServerCacheControl.REVALIDATED)
882+
.autoDecompress(true)
883+
.serveCompressedFiles(true)
884+
.build().orElse(fallbackFileService));
885+
886+
// Serve all web resources except for '/app'.
887+
sb.route()
888+
.pathPrefix("/")
889+
.exclude("/app")
890+
.build(FileService.builder(CentralDogma.class.getClassLoader(),
891+
"com/linecorp/centraldogma/webapp")
892+
.cacheControl(ServerCacheControl.REVALIDATED)
893+
.autoDecompress(true)
894+
.serveCompressedFiles(true)
895+
.build());
896+
897+
sb.serviceUnder("/legacy-web",
883898
FileService.builder(CentralDogma.class.getClassLoader(), "webapp")
884899
.cacheControl(ServerCacheControl.REVALIDATED)
900+
.autoDecompress(true)
901+
.serveCompressedFiles(true)
885902
.build());
886903
}
887904

server/src/main/java/com/linecorp/centraldogma/server/auth/AuthProvider.java

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@
2525
import com.google.common.collect.ImmutableList;
2626
import com.google.common.collect.ImmutableSet;
2727

28-
import com.linecorp.armeria.common.HttpHeaderNames;
2928
import com.linecorp.armeria.common.HttpResponse;
3029
import com.linecorp.armeria.common.HttpStatus;
31-
import com.linecorp.armeria.common.ResponseHeaders;
3230
import com.linecorp.armeria.server.HttpService;
3331
import com.linecorp.armeria.server.HttpServiceWithRoutes;
3432
import com.linecorp.armeria.server.Route;
@@ -42,13 +40,14 @@ public interface AuthProvider {
4240
* A login page path for the web console. If a user, who has not logged into the web console yet,
4341
* opens the web console, the web browser would bring the user to the login page.
4442
*/
45-
String LOGIN_PATH = "/link/auth/login";
43+
// TODO(ikhoon): Remove the trailing slash once https://github.com/line/armeria/issues/4542 is resolved.
44+
String LOGIN_PATH = "/link/auth/login/";
4645

4746
/**
4847
* A logout page path for the web console. If a user clicks the logout button on the navigation bar,
4948
* the web browser would bring the user to the logout page.
5049
*/
51-
String LOGOUT_PATH = "/link/auth/logout";
50+
String LOGOUT_PATH = "/link/auth/logout/";
5251

5352
/**
5453
* A base path of the built-in web app.
@@ -58,12 +57,12 @@ public interface AuthProvider {
5857
/**
5958
* A path which provides a built-in HTML login form to a user.
6059
*/
61-
String BUILTIN_WEB_LOGIN_PATH = BUILTIN_WEB_BASE_PATH + "/login";
60+
String BUILTIN_WEB_LOGIN_PATH = BUILTIN_WEB_BASE_PATH + "/login/";
6261

6362
/**
6463
* A path which provides a built-in HTML logout page to a user.
6564
*/
66-
String BUILTIN_WEB_LOGOUT_PATH = BUILTIN_WEB_BASE_PATH + "/logout";
65+
String BUILTIN_WEB_LOGOUT_PATH = BUILTIN_WEB_BASE_PATH + "/logout/";
6766

6867
/**
6968
* A set of {@link Route}s which handles a login request. It is necessary only if
@@ -86,10 +85,16 @@ public interface AuthProvider {
8685
* the browser would bring a user to the built-in web login page served on {@value BUILTIN_WEB_LOGIN_PATH}.
8786
*/
8887
default HttpService webLoginService() {
89-
// Redirect to the default page: /link/auth/login -> /web/auth/login
90-
return (ctx, req) -> HttpResponse.of(
91-
ResponseHeaders.of(HttpStatus.MOVED_PERMANENTLY, HttpHeaderNames.LOCATION,
92-
BUILTIN_WEB_LOGIN_PATH));
88+
// Redirect to the default page: /link/auth/login -> /web/auth/login/
89+
return (ctx, req) -> {
90+
String returnTo = ctx.queryParam("return_to");
91+
if (returnTo != null) {
92+
returnTo += BUILTIN_WEB_LOGIN_PATH;
93+
} else {
94+
returnTo = BUILTIN_WEB_LOGIN_PATH;
95+
}
96+
return HttpResponse.ofRedirect(HttpStatus.MOVED_PERMANENTLY, returnTo);
97+
};
9398
}
9499

95100
/**
@@ -98,10 +103,16 @@ default HttpService webLoginService() {
98103
* {@value BUILTIN_WEB_LOGOUT_PATH}.
99104
*/
100105
default HttpService webLogoutService() {
101-
// Redirect to the default page: /link/auth/logout -> /web/auth/logout
102-
return (ctx, req) -> HttpResponse.of(
103-
ResponseHeaders.of(HttpStatus.MOVED_PERMANENTLY, HttpHeaderNames.LOCATION,
104-
BUILTIN_WEB_LOGOUT_PATH));
106+
// Redirect to the default page: /link/auth/logout -> /web/auth/logout/
107+
return (ctx, req) -> {
108+
String returnTo = ctx.queryParam("return_to");
109+
if (returnTo != null) {
110+
returnTo += BUILTIN_WEB_LOGOUT_PATH;
111+
} else {
112+
returnTo = BUILTIN_WEB_LOGOUT_PATH;
113+
}
114+
return HttpResponse.ofRedirect(HttpStatus.MOVED_PERMANENTLY, returnTo);
115+
};
105116
}
106117

107118
/**

server/src/main/java/com/linecorp/centraldogma/server/internal/api/CredentialServiceV1.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package com.linecorp.centraldogma.server.internal.api;
1818

19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static com.google.common.collect.ImmutableList.toImmutableList;
21+
1922
import java.util.List;
2023
import java.util.concurrent.CompletableFuture;
2124

@@ -32,6 +35,7 @@
3235
import com.linecorp.centraldogma.server.internal.api.auth.RequiresReadPermission;
3336
import com.linecorp.centraldogma.server.internal.api.auth.RequiresWritePermission;
3437
import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager;
38+
import com.linecorp.centraldogma.server.metadata.User;
3539
import com.linecorp.centraldogma.server.mirror.MirrorCredential;
3640
import com.linecorp.centraldogma.server.storage.project.Project;
3741
import com.linecorp.centraldogma.server.storage.repository.MetaRepository;
@@ -56,8 +60,18 @@ public CredentialServiceV1(ProjectApiManager projectApiManager, CommandExecutor
5660
*/
5761
@RequiresReadPermission(repository = Project.REPO_META)
5862
@Get("/projects/{projectName}/credentials")
59-
public CompletableFuture<List<MirrorCredential>> listCredentials(@Param String projectName) {
60-
return metaRepo(projectName).credentials();
63+
public CompletableFuture<List<MirrorCredential>> listCredentials(User loginUser,
64+
@Param String projectName) {
65+
final CompletableFuture<List<MirrorCredential>> future = metaRepo(projectName).credentials();
66+
if (loginUser.isAdmin()) {
67+
return future;
68+
}
69+
return future.thenApply(credentials -> {
70+
return credentials
71+
.stream()
72+
.map(MirrorCredential::withoutSecret)
73+
.collect(toImmutableList());
74+
});
6175
}
6276

6377
/**
@@ -67,8 +81,13 @@ public CompletableFuture<List<MirrorCredential>> listCredentials(@Param String p
6781
*/
6882
@RequiresReadPermission(repository = Project.REPO_META)
6983
@Get("/projects/{projectName}/credentials/{id}")
70-
public CompletableFuture<MirrorCredential> getCredentialById(@Param String projectName, @Param String id) {
71-
return metaRepo(projectName).credential(id);
84+
public CompletableFuture<MirrorCredential> getCredentialById(User loginUser,
85+
@Param String projectName, @Param String id) {
86+
final CompletableFuture<MirrorCredential> future = metaRepo(projectName).credential(id);
87+
if (loginUser.isAdmin()) {
88+
return future;
89+
}
90+
return future.thenApply(MirrorCredential::withoutSecret);
7291
}
7392

7493
/**
@@ -86,15 +105,17 @@ public CompletableFuture<PushResultDto> createCredential(@Param String projectNa
86105
}
87106

88107
/**
89-
* PUT /projects/{projectName}/credentials
108+
* PUT /projects/{projectName}/credentials/{id}
90109
*
91110
* <p>Update the existing credential.
92111
*/
93112
@RequiresWritePermission(repository = Project.REPO_META)
94-
@Put("/projects/{projectName}/credentials")
113+
@Put("/projects/{projectName}/credentials/{id}")
95114
@ConsumesJson
96115
public CompletableFuture<PushResultDto> updateCredential(@Param String projectName,
116+
@Param String id,
97117
MirrorCredential credential, Author author) {
118+
checkArgument(id.equals(credential.id()), "The credential ID (%s) can't be updated", id);
98119
return createOrUpdate(projectName, credential, author, true);
99120
}
100121

server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.linecorp.centraldogma.server.internal.api;
1818

19+
import static com.google.common.base.Preconditions.checkArgument;
1920
import static com.google.common.collect.ImmutableList.toImmutableList;
2021

2122
import java.net.URI;
@@ -106,10 +107,11 @@ public CompletableFuture<PushResultDto> createMirror(@Param String projectName,
106107
* <p>Update the exising mirror.
107108
*/
108109
@RequiresWritePermission(repository = Project.REPO_META)
109-
@Put("/projects/{projectName}/mirrors")
110+
@Put("/projects/{projectName}/mirrors/{id}")
110111
@ConsumesJson
111112
public CompletableFuture<PushResultDto> updateMirror(@Param String projectName, MirrorDto mirror,
112-
Author author) {
113+
@Param String id, Author author) {
114+
checkArgument(id.equals(mirror.id()), "The mirror ID (%s) can't be updated", id);
113115
return createOrUpdate(projectName, mirror, author, true);
114116
}
115117

server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/credential/AccessTokenMirrorCredential.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
3232
import com.google.common.base.MoreObjects.ToStringHelper;
3333

34+
import com.linecorp.centraldogma.server.mirror.MirrorCredential;
35+
3436
public final class AccessTokenMirrorCredential extends AbstractMirrorCredential {
3537

3638
private static final Logger logger = LoggerFactory.getLogger(AccessTokenMirrorCredential.class);
@@ -93,4 +95,9 @@ public boolean equals(Object o) {
9395
void addProperties(ToStringHelper helper) {
9496
// Access token must be kept secret.
9597
}
98+
99+
@Override
100+
public MirrorCredential withoutSecret() {
101+
return new AccessTokenMirrorCredential(id(), enabled(), hostnamePatterns(), "****");
102+
}
96103
}

server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/credential/NoneMirrorCredential.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2626
import com.google.common.base.MoreObjects.ToStringHelper;
2727

28+
import com.linecorp.centraldogma.server.mirror.MirrorCredential;
29+
2830
public final class NoneMirrorCredential extends AbstractMirrorCredential {
2931

3032
@JsonCreator
@@ -40,4 +42,9 @@ public NoneMirrorCredential(@JsonProperty("id") String id,
4042
void addProperties(ToStringHelper helper) {
4143
// No properties to add
4244
}
45+
46+
@Override
47+
public MirrorCredential withoutSecret() {
48+
return this;
49+
}
4350
}

server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/credential/PasswordMirrorCredential.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
3333
import com.google.common.base.MoreObjects.ToStringHelper;
3434

35+
import com.linecorp.centraldogma.server.mirror.MirrorCredential;
36+
3537
public final class PasswordMirrorCredential extends AbstractMirrorCredential {
3638

3739
private static final Logger logger = LoggerFactory.getLogger(PasswordMirrorCredential.class);
@@ -64,7 +66,7 @@ public String password() {
6466
} catch (Throwable t) {
6567
// The password probably has `:` without prefix. Just return it as is for backward compatibility.
6668
logger.debug("Failed to convert the password of the credential. username: {}, id: {}",
67-
username, id(), t);
69+
username, id(), t);
6870
return password;
6971
}
7072
}
@@ -104,4 +106,9 @@ public boolean equals(Object o) {
104106
void addProperties(ToStringHelper helper) {
105107
helper.add("username", username);
106108
}
109+
110+
@Override
111+
public MirrorCredential withoutSecret() {
112+
return new PasswordMirrorCredential(id(), enabled(), hostnamePatterns(), username(), "****");
113+
}
107114
}

0 commit comments

Comments
 (0)