add a setup landing page instead of creating a default admin account

This commit is contained in:
Athou
2026-01-10 17:14:04 +01:00
parent ab3d41508f
commit a080ede15b
64 changed files with 1178 additions and 186 deletions

View File

@@ -43,4 +43,7 @@ public class ServerInfo implements Serializable {
@Schema(required = true)
private long forceRefreshCooldownDuration;
@Schema(required = true)
private boolean initialSetupRequired;
}

View File

@@ -0,0 +1,25 @@
package com.commafeed.frontend.model.request;
import java.io.Serializable;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import com.commafeed.security.password.ValidPassword;
import lombok.Data;
@SuppressWarnings("serial")
@Schema(description = "Initial admin account setup request")
@Data
public class InitialSetupRequest implements Serializable {
@Schema(description = "admin username", required = true)
private String name;
@Schema(description = "admin password", required = true)
@ValidPassword
private String password;
@Schema(description = "admin email")
private String email;
}

View File

@@ -26,7 +26,6 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConstants;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.model.User;
@@ -101,8 +100,8 @@ public class AdminREST {
if (req.isAdmin() && !roles.contains(Role.ADMIN)) {
userRoleDAO.persist(new UserRole(u, Role.ADMIN));
} else if (!req.isAdmin() && roles.contains(Role.ADMIN)) {
if (CommaFeedConstants.USERNAME_ADMIN.equals(u.getName())) {
return Response.status(Status.FORBIDDEN).entity("You cannot remove the admin role from the admin user.").build();
if (userRoleDAO.countAdmins() == 1) {
return Response.status(Status.FORBIDDEN).entity("You cannot remove the admin role from the last admin user.").build();
}
for (UserRole userRole : userRoleDAO.findAll(u)) {
if (userRole.getRole() == Role.ADMIN) {

View File

@@ -22,6 +22,7 @@ import com.commafeed.CommaFeedVersion;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.feed.ImageProxyUrl;
import com.commafeed.backend.service.db.DatabaseStartupService;
import com.commafeed.frontend.model.ServerInfo;
import com.commafeed.security.Roles;
@@ -39,6 +40,7 @@ public class ServerREST {
private final HttpGetter httpGetter;
private final CommaFeedConfiguration config;
private final CommaFeedVersion version;
private final DatabaseStartupService databaseStartupService;
@Path("/get")
@GET
@@ -57,6 +59,7 @@ public class ServerREST {
infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis());
infos.setForceRefreshCooldownDuration(config.feedRefresh().forceRefreshCooldownDuration().toMillis());
infos.setInitialSetupRequired(databaseStartupService.isInitialSetupRequired());
return infos;
}

View File

@@ -5,7 +5,9 @@ import java.net.URISyntaxException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import jakarta.annotation.security.PermitAll;
@@ -36,6 +38,7 @@ import com.commafeed.CommaFeedConfiguration;
import com.commafeed.CommaFeedConstants;
import com.commafeed.backend.Digests;
import com.commafeed.backend.Urls;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.dao.UserSettingsDAO;
@@ -50,8 +53,10 @@ import com.commafeed.backend.model.UserSettings.ScrollMode;
import com.commafeed.backend.service.MailService;
import com.commafeed.backend.service.PasswordEncryptionService;
import com.commafeed.backend.service.UserService;
import com.commafeed.backend.service.db.DatabaseStartupService;
import com.commafeed.frontend.model.Settings;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.InitialSetupRequest;
import com.commafeed.frontend.model.request.PasswordResetRequest;
import com.commafeed.frontend.model.request.ProfileModificationRequest;
import com.commafeed.frontend.model.request.RegistrationRequest;
@@ -78,9 +83,11 @@ public class UserREST {
private final UserSettingsDAO userSettingsDAO;
private final UserService userService;
private final PasswordEncryptionService encryptionService;
private final DatabaseStartupService databaseStartupService;
private final MailService mailService;
private final CommaFeedConfiguration config;
private final UriInfo uri;
private final UnitOfWork unitOfWork;
@Path("/settings")
@GET
@@ -231,7 +238,7 @@ public class UserREST {
public Response saveUserProfile(@Valid @Parameter(required = true) ProfileModificationRequest request) {
User user = authenticationContext.getCurrentUser();
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
return Response.status(Status.FORBIDDEN).build();
return Response.status(Status.FORBIDDEN).entity("the profile of the demo account cannot be modified").build();
}
Optional<User> login = userService.login(user.getName(), request.getCurrentPassword());
@@ -276,6 +283,31 @@ public class UserREST {
}
}
@Path("/initialSetup")
@PermitAll
@POST
@Transactional
@Operation(
summary = "Create the initial admin account",
description = "This endpoint is only available when no users exist in the database")
public Response initialSetup(@Valid @Parameter(required = true) InitialSetupRequest req) {
boolean initialSetupRequired = databaseStartupService.isInitialSetupRequired();
if (!initialSetupRequired) {
return Response.status(Status.BAD_REQUEST).entity("Initial setup has already been completed").build();
}
userService.register(req.getName(), req.getPassword(), req.getEmail(), List.of(Role.ADMIN, Role.USER), true);
if (config.users().createDemoAccount()) {
User demo = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO);
if (demo == null) {
userService.createDemoUser();
}
}
return Response.ok().build();
}
@Path("/passwordReset")
@PermitAll
@POST
@@ -361,9 +393,15 @@ public class UserREST {
@Operation(summary = "Delete the user account")
public Response deleteUser() {
User user = authenticationContext.getCurrentUser();
if (CommaFeedConstants.USERNAME_ADMIN.equals(user.getName()) || CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
return Response.status(Status.FORBIDDEN).build();
if (CommaFeedConstants.USERNAME_DEMO.equals(user.getName())) {
return Response.status(Status.FORBIDDEN).entity("the demo account cannot be deleted").build();
}
Set<Role> roles = userRoleDAO.findRoles(user);
if (roles.contains(Role.ADMIN) && userRoleDAO.countAdmins() == 1) {
return Response.status(Status.FORBIDDEN).entity("The last admin account cannot be deleted").build();
}
userService.unregister(userDAO.findById(user.getId()));
return Response.ok().build();
}

View File

@@ -1,8 +1,5 @@
package com.commafeed.frontend.servlet;
import java.time.Instant;
import java.util.Date;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Singleton;
import jakarta.ws.rs.GET;
@@ -11,26 +8,25 @@ import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import com.commafeed.security.CookieService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Path("/logout")
@PermitAll
@Singleton
public class LogoutServlet {
private final UriInfo uri;
private final String cookieName;
public LogoutServlet(UriInfo uri, @ConfigProperty(name = "quarkus.http.auth.form.cookie-name") String cookieName) {
this.uri = uri;
this.cookieName = cookieName;
}
private final CookieService cookieService;
@GET
@Operation(hidden = true)
public Response get() {
NewCookie removeCookie = new NewCookie.Builder(cookieName).maxAge(0).expiry(Date.from(Instant.EPOCH)).path("/").build();
NewCookie removeCookie = cookieService.buildLogoutCookie();
return Response.temporaryRedirect(uri.getBaseUri()).cookie(removeCookie).build();
}
}