mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
WIP
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
package com.commafeed.frontend.auth;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.passay.CharacterRule;
|
||||
import org.passay.EnglishCharacterData;
|
||||
import org.passay.LengthRule;
|
||||
import org.passay.PasswordData;
|
||||
import org.passay.PasswordValidator;
|
||||
import org.passay.RuleResult;
|
||||
import org.passay.WhitespaceRule;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import lombok.Setter;
|
||||
|
||||
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {
|
||||
|
||||
@Setter
|
||||
private static boolean strict = true;
|
||||
|
||||
@Override
|
||||
public void initialize(ValidPassword constraintAnnotation) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
PasswordValidator validator = strict ? buildStrictPasswordValidator() : buildLoosePasswordValidator();
|
||||
RuleResult result = validator.validate(new PasswordData(value));
|
||||
|
||||
if (result.isValid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> messages = validator.getMessages(result);
|
||||
String message = String.join(System.lineSeparator(), messages);
|
||||
context.buildConstraintViolationWithTemplate(message).addConstraintViolation().disableDefaultConstraintViolation();
|
||||
return false;
|
||||
}
|
||||
|
||||
private PasswordValidator buildStrictPasswordValidator() {
|
||||
return new PasswordValidator(
|
||||
// length
|
||||
new LengthRule(8, 256),
|
||||
// 1 uppercase char
|
||||
new CharacterRule(EnglishCharacterData.UpperCase, 1),
|
||||
// 1 lowercase char
|
||||
new CharacterRule(EnglishCharacterData.LowerCase, 1),
|
||||
// 1 digit
|
||||
new CharacterRule(EnglishCharacterData.Digit, 1),
|
||||
// 1 special char
|
||||
new CharacterRule(EnglishCharacterData.Special, 1),
|
||||
// no whitespace
|
||||
new WhitespaceRule());
|
||||
}
|
||||
|
||||
private PasswordValidator buildLoosePasswordValidator() {
|
||||
return new PasswordValidator(
|
||||
// length
|
||||
new LengthRule(6, 256),
|
||||
// no whitespace
|
||||
new WhitespaceRule());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.commafeed.frontend.auth;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
|
||||
@Inherited
|
||||
@Target({ ElementType.PARAMETER })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SecurityCheck {
|
||||
|
||||
/**
|
||||
* Roles needed.
|
||||
*/
|
||||
Role value() default Role.USER;
|
||||
|
||||
boolean apiKeyAllowed() default false;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.commafeed.frontend.auth;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.frontend.session.SessionHelper;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityCheckFactory implements Function<ContainerRequest, User> {
|
||||
|
||||
private static final String PREFIX = "Basic";
|
||||
|
||||
private final UserDAO userDAO;
|
||||
private final UserService userService;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final HttpServletRequest request;
|
||||
private final Role role;
|
||||
private final boolean apiKeyAllowed;
|
||||
|
||||
@Override
|
||||
public User apply(ContainerRequest req) {
|
||||
Optional<User> user = apiKeyLogin();
|
||||
if (user.isEmpty()) {
|
||||
user = basicAuthenticationLogin();
|
||||
}
|
||||
if (user.isEmpty()) {
|
||||
user = cookieSessionLogin(new SessionHelper(request));
|
||||
}
|
||||
|
||||
if (user.isPresent()) {
|
||||
Set<Role> roles = userService.getRoles(user.get());
|
||||
if (roles.contains(role)) {
|
||||
return user.get();
|
||||
} else {
|
||||
throw buildWebApplicationException(Response.Status.FORBIDDEN, "You don't have the required role to access this resource.");
|
||||
}
|
||||
} else {
|
||||
throw buildWebApplicationException(Response.Status.UNAUTHORIZED, "Credentials are required to access this resource.");
|
||||
}
|
||||
}
|
||||
|
||||
Optional<User> cookieSessionLogin(SessionHelper sessionHelper) {
|
||||
Optional<User> loggedInUser = sessionHelper.getLoggedInUserId().map(userDAO::findById);
|
||||
loggedInUser.ifPresent(userService::performPostLoginActivities);
|
||||
return loggedInUser;
|
||||
}
|
||||
|
||||
private Optional<User> basicAuthenticationLogin() {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (header != null) {
|
||||
int space = header.indexOf(' ');
|
||||
if (space > 0) {
|
||||
String method = header.substring(0, space);
|
||||
if (PREFIX.equalsIgnoreCase(method)) {
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(header.substring(space + 1));
|
||||
String decoded = new String(decodedBytes, StandardCharsets.ISO_8859_1);
|
||||
int i = decoded.indexOf(':');
|
||||
if (i > 0) {
|
||||
String username = decoded.substring(0, i);
|
||||
String password = decoded.substring(i + 1);
|
||||
return userService.login(username, password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<User> apiKeyLogin() {
|
||||
String apiKey = request.getParameter("apiKey");
|
||||
if (apiKey != null && apiKeyAllowed) {
|
||||
return userService.login(apiKey);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private WebApplicationException buildWebApplicationException(Response.Status status, String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("message", message);
|
||||
response.put("allowRegistrations", config.getApplicationSettings().getAllowRegistrations());
|
||||
return new WebApplicationException(Response.status(status).entity(response).type(MediaType.APPLICATION_JSON).build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package com.commafeed.frontend.auth;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.glassfish.hk2.utilities.binding.AbstractBinder;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider;
|
||||
import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
|
||||
import org.glassfish.jersey.server.model.Parameter;
|
||||
import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Singleton
|
||||
public class SecurityCheckFactoryProvider extends AbstractValueParamProvider {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserDAO userDAO;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final HttpServletRequest request;
|
||||
|
||||
@Inject
|
||||
public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, UserDAO userDAO,
|
||||
UserService userService, CommaFeedConfiguration config, HttpServletRequest request) {
|
||||
super(() -> extractorProvider, Parameter.Source.UNKNOWN);
|
||||
this.userDAO = userDAO;
|
||||
this.userService = userService;
|
||||
this.config = config;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Function<ContainerRequest, ?> createValueProvider(Parameter parameter) {
|
||||
final Class<?> classType = parameter.getRawType();
|
||||
|
||||
SecurityCheck securityCheck = parameter.getAnnotation(SecurityCheck.class);
|
||||
if (securityCheck == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!classType.isAssignableFrom(User.class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SecurityCheckFactory(userDAO, userService, config, request, securityCheck.value(), securityCheck.apiKeyAllowed());
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public static class Binder extends AbstractBinder {
|
||||
|
||||
private final UserDAO userDAO;
|
||||
private final UserService userService;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(SecurityCheckFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class);
|
||||
bind(userDAO).to(UserDAO.class);
|
||||
bind(userService).to(UserService.class);
|
||||
bind(config).to(CommaFeedConfiguration.class);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.commafeed.frontend.auth;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
@Documented
|
||||
@Constraint(validatedBy = PasswordConstraintValidator.class)
|
||||
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ValidPassword {
|
||||
|
||||
String message() default "Invalid Password";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -11,6 +12,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Entry details")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Category implements Serializable {
|
||||
|
||||
@Schema(description = "category id", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -11,6 +12,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "List of entries with some metadata")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Entries implements Serializable {
|
||||
|
||||
@Schema(description = "name of the feed or the category requested", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.rometools.rome.feed.synd.SyndEnclosureImpl;
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.SyndEntryImpl;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -26,6 +27,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Entry details")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Entry implements Serializable {
|
||||
|
||||
@Schema(description = "entry id", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -9,6 +10,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Feed details")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class FeedInfo implements Serializable {
|
||||
|
||||
@Schema(description = "url", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -9,6 +10,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Server infos")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class ServerInfo implements Serializable {
|
||||
|
||||
@Schema
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.frontend.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -9,6 +10,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User settings")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Settings implements Serializable {
|
||||
|
||||
@Schema(description = "user's preferred language, english if none", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -15,6 +16,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User information")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class Subscription implements Serializable {
|
||||
|
||||
@Schema(description = "subscription id", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -3,12 +3,14 @@ package com.commafeed.frontend.model;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "Unread count")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class UnreadCount implements Serializable {
|
||||
|
||||
@Schema
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.commafeed.frontend.model;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import lombok.Data;
|
||||
@@ -10,6 +11,7 @@ import lombok.Data;
|
||||
@SuppressWarnings("serial")
|
||||
@Schema(description = "User information")
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public class UserModel implements Serializable {
|
||||
|
||||
@Schema(description = "user id", requiredMode = RequiredMode.REQUIRED)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@Schema
|
||||
public class LoginRequest implements Serializable {
|
||||
|
||||
@Schema(description = "username", requiredMode = RequiredMode.REQUIRED)
|
||||
@Size(min = 3, max = 32)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "password", requiredMode = RequiredMode.REQUIRED)
|
||||
@NotEmpty
|
||||
@Size(max = 128)
|
||||
private String password;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.commafeed.frontend.auth.ValidPassword;
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.commafeed.frontend.auth.ValidPassword;
|
||||
import com.commafeed.security.password.ValidPassword;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
|
||||
@@ -7,10 +7,7 @@ import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.commafeed.CommaFeedApplication;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserRoleDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
@@ -18,14 +15,14 @@ import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.service.PasswordEncryptionService;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.frontend.auth.SecurityCheck;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
import com.commafeed.frontend.model.request.AdminSaveUserRequest;
|
||||
import com.commafeed.frontend.model.request.IDRequest;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import com.commafeed.security.Roles;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import io.dropwizard.hibernate.UnitOfWork;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
@@ -33,8 +30,9 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
@@ -46,30 +44,29 @@ import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Path("/admin")
|
||||
@Path("/rest/admin")
|
||||
@RolesAllowed(Roles.ADMIN)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Tag(name = "Admin")
|
||||
public class AdminREST {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserDAO userDAO;
|
||||
private final UserRoleDAO userRoleDAO;
|
||||
private final UserService userService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final MetricRegistry metrics;
|
||||
|
||||
@Path("/user/save")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Save or update a user",
|
||||
description = "Save or update a user. If the id is not specified, a new user will be created")
|
||||
@Timed
|
||||
public Response adminSaveUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
|
||||
@Parameter(required = true) AdminSaveUserRequest req) {
|
||||
public Response adminSaveUser(@Parameter(required = true) AdminSaveUserRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getName());
|
||||
|
||||
@@ -87,6 +84,7 @@ public class AdminREST {
|
||||
return Response.status(Status.CONFLICT).entity(e.getMessage()).build();
|
||||
}
|
||||
} else {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (req.getId().equals(user.getId()) && !req.isEnabled()) {
|
||||
return Response.status(Status.FORBIDDEN).entity("You cannot disable your own account.").build();
|
||||
}
|
||||
@@ -121,14 +119,12 @@ public class AdminREST {
|
||||
|
||||
@Path("/user/get/{id}")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get user information",
|
||||
description = "Get user information",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = UserModel.class))) })
|
||||
@Timed
|
||||
public Response adminGetUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
|
||||
@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
public Response adminGetUser(@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
User u = userDAO.findById(id);
|
||||
UserModel userModel = new UserModel();
|
||||
@@ -142,13 +138,12 @@ public class AdminREST {
|
||||
|
||||
@Path("/user/getAll")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get all users",
|
||||
description = "Get all users",
|
||||
responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserModel.class)))) })
|
||||
@Timed
|
||||
public Response adminGetUsers(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) {
|
||||
public Response adminGetUsers() {
|
||||
Map<Long, UserModel> users = new HashMap<>();
|
||||
for (UserRole role : userRoleDAO.findAll()) {
|
||||
User u = role.getUser();
|
||||
@@ -173,11 +168,9 @@ public class AdminREST {
|
||||
|
||||
@Path("/user/delete")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Delete a user", description = "Delete a user, and all his subscriptions")
|
||||
@Timed
|
||||
public Response adminDeleteUser(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user,
|
||||
@Parameter(required = true) IDRequest req) {
|
||||
public Response adminDeleteUser(@Parameter(required = true) IDRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@@ -185,6 +178,8 @@ public class AdminREST {
|
||||
if (u == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user.getId().equals(u.getId())) {
|
||||
return Response.status(Status.FORBIDDEN).entity("You cannot delete your own user.").build();
|
||||
}
|
||||
@@ -192,24 +187,11 @@ public class AdminREST {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/settings")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Operation(
|
||||
summary = "Retrieve application settings",
|
||||
description = "Retrieve application settings",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = ApplicationSettings.class))) })
|
||||
@Timed
|
||||
public Response getApplicationSettings(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) {
|
||||
return Response.ok(config.getApplicationSettings()).build();
|
||||
}
|
||||
|
||||
@Path("/metrics")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Retrieve server metrics")
|
||||
@Timed
|
||||
public Response getMetrics(@Parameter(hidden = true) @SecurityCheck(Role.ADMIN) User user) {
|
||||
public Response getMetrics() {
|
||||
return Response.ok(metrics).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
@@ -29,7 +28,6 @@ import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
import com.commafeed.backend.service.FeedSubscriptionService;
|
||||
import com.commafeed.frontend.auth.SecurityCheck;
|
||||
import com.commafeed.frontend.model.Category;
|
||||
import com.commafeed.frontend.model.Entries;
|
||||
import com.commafeed.frontend.model.Entry;
|
||||
@@ -40,13 +38,14 @@ import com.commafeed.frontend.model.request.CategoryModificationRequest;
|
||||
import com.commafeed.frontend.model.request.CollapseRequest;
|
||||
import com.commafeed.frontend.model.request.IDRequest;
|
||||
import com.commafeed.frontend.model.request.MarkRequest;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import com.commafeed.security.Roles;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.feed.synd.SyndFeedImpl;
|
||||
import com.rometools.rome.io.SyndFeedOutput;
|
||||
|
||||
import io.dropwizard.hibernate.UnitOfWork;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
@@ -54,8 +53,9 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
@@ -70,11 +70,12 @@ import jakarta.ws.rs.core.Response.Status;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Path("/category")
|
||||
@Path("/rest/category")
|
||||
@RolesAllowed(Roles.USER)
|
||||
@Slf4j
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Tag(name = "Feed categories")
|
||||
public class CategoryREST {
|
||||
@@ -82,6 +83,7 @@ public class CategoryREST {
|
||||
public static final String ALL = "all";
|
||||
public static final String STARRED = "starred";
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
@@ -92,13 +94,12 @@ public class CategoryREST {
|
||||
|
||||
@Path("/entries")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get category entries",
|
||||
description = "Get a list of category entries",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Entries.class))) })
|
||||
@Timed
|
||||
public Response getCategoryEntries(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
|
||||
public Response getCategoryEntries(
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
|
||||
@Parameter(
|
||||
description = "all entries or only unread ones",
|
||||
@@ -137,22 +138,24 @@ public class CategoryREST {
|
||||
excludedIds = Arrays.stream(excludedSubscriptionIds.split(",")).map(Long::valueOf).toList();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (ALL.equals(id)) {
|
||||
entries.setName(Optional.ofNullable(tag).orElse("All"));
|
||||
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, excludedIds);
|
||||
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(user, subs, unreadOnly, entryKeywords, newerThanDate,
|
||||
offset, limit + 1, order, true, tag, null, null);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
|
||||
} else if (STARRED.equals(id)) {
|
||||
entries.setName("Starred");
|
||||
List<FeedEntryStatus> starred = feedEntryStatusDAO.findStarred(user, newerThanDate, offset, limit + 1, order, true);
|
||||
for (FeedEntryStatus status : starred) {
|
||||
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
} else {
|
||||
FeedCategory parent = feedCategoryDAO.findById(user, Long.valueOf(id));
|
||||
@@ -164,7 +167,7 @@ public class CategoryREST {
|
||||
offset, limit + 1, order, true, tag, null, null);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
entries.setName(parent.getName());
|
||||
} else {
|
||||
@@ -186,11 +189,10 @@ public class CategoryREST {
|
||||
|
||||
@Path("/entriesAsFeed")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Get category entries as feed", description = "Get a feed of category entries")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
@Timed
|
||||
public Response getCategoryEntriesAsFeed(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
|
||||
public Response getCategoryEntriesAsFeed(
|
||||
@Parameter(description = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
|
||||
@Parameter(
|
||||
description = "all entries or only unread ones",
|
||||
@@ -205,7 +207,7 @@ public class CategoryREST {
|
||||
description = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds,
|
||||
@Parameter(description = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
|
||||
|
||||
Response response = getCategoryEntries(user, id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag);
|
||||
Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, excludedSubscriptionIds, tag);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
@@ -215,7 +217,7 @@ public class CategoryREST {
|
||||
feed.setFeedType("rss_2.0");
|
||||
feed.setTitle("CommaFeed - " + entries.getName());
|
||||
feed.setDescription("CommaFeed - " + entries.getName());
|
||||
feed.setLink(config.getApplicationSettings().getPublicUrl());
|
||||
feed.setLink(config.publicUrl());
|
||||
feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList());
|
||||
|
||||
SyndFeedOutput output = new SyndFeedOutput();
|
||||
@@ -231,11 +233,9 @@ public class CategoryREST {
|
||||
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Mark category entries", description = "Mark feed entries of this category as read")
|
||||
@Timed
|
||||
public Response markCategoryEntries(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) {
|
||||
public Response markCategoryEntries(@Valid @Parameter(description = "category id, or 'all'", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@@ -244,6 +244,7 @@ public class CategoryREST {
|
||||
String keywords = req.getKeywords();
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (ALL.equals(req.getId())) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
removeExcludedSubscriptions(subs, req.getExcludedSubscriptions());
|
||||
@@ -268,17 +269,17 @@ public class CategoryREST {
|
||||
|
||||
@Path("/add")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Add a category",
|
||||
description = "Add a new feed category",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Long.class))) })
|
||||
@Timed
|
||||
public Response addCategory(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(required = true) AddCategoryRequest req) {
|
||||
public Response addCategory(@Valid @Parameter(required = true) AddCategoryRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getName());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
|
||||
FeedCategory cat = new FeedCategory();
|
||||
cat.setName(req.getName());
|
||||
cat.setUser(user);
|
||||
@@ -296,14 +297,14 @@ public class CategoryREST {
|
||||
|
||||
@POST
|
||||
@Path("/delete")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Delete a category", description = "Delete an existing feed category")
|
||||
@Timed
|
||||
public Response deleteCategory(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) IDRequest req) {
|
||||
public Response deleteCategory(@Parameter(required = true) IDRequest req) {
|
||||
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory cat = feedCategoryDAO.findById(user, req.getId());
|
||||
if (cat != null) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategory(user, cat);
|
||||
@@ -329,14 +330,13 @@ public class CategoryREST {
|
||||
|
||||
@POST
|
||||
@Path("/modify")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Rename a category", description = "Rename an existing feed category")
|
||||
@Timed
|
||||
public Response modifyCategory(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(required = true) CategoryModificationRequest req) {
|
||||
public Response modifyCategory(@Valid @Parameter(required = true) CategoryModificationRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory category = feedCategoryDAO.findById(user, req.getId());
|
||||
|
||||
if (StringUtils.isNotBlank(req.getName())) {
|
||||
@@ -380,13 +380,13 @@ public class CategoryREST {
|
||||
|
||||
@POST
|
||||
@Path("/collapse")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Collapse a category", description = "Save collapsed or expanded status for a category")
|
||||
@Timed
|
||||
public Response collapseCategory(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) CollapseRequest req) {
|
||||
public Response collapseCategory(@Parameter(required = true) CollapseRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedCategory category = feedCategoryDAO.findById(user, req.getId());
|
||||
if (category == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
@@ -399,25 +399,25 @@ public class CategoryREST {
|
||||
|
||||
@GET
|
||||
@Path("/unreadCount")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get unread count for feed subscriptions",
|
||||
responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = UnreadCount.class)))) })
|
||||
@Timed
|
||||
public Response getUnreadCount(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user) {
|
||||
public Response getUnreadCount() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
Map<Long, UnreadCount> unreadCount = feedSubscriptionService.getUnreadCount(user);
|
||||
return Response.ok(Lists.newArrayList(unreadCount.values())).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/get")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get root category",
|
||||
description = "Get all categories and subscriptions of the user",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Category.class))) })
|
||||
@Timed
|
||||
public Response getRootCategory(@Parameter(hidden = true) @SecurityCheck User user) {
|
||||
public Response getRootCategory() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
Category root = cache.getUserRootCategory(user);
|
||||
if (root == null) {
|
||||
log.debug("tree cache miss for {}", user.getId());
|
||||
|
||||
@@ -2,24 +2,24 @@ package com.commafeed.frontend.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.commafeed.backend.dao.FeedEntryTagDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
import com.commafeed.backend.service.FeedEntryTagService;
|
||||
import com.commafeed.frontend.auth.SecurityCheck;
|
||||
import com.commafeed.frontend.model.request.MarkRequest;
|
||||
import com.commafeed.frontend.model.request.MultipleMarkRequest;
|
||||
import com.commafeed.frontend.model.request.StarRequest;
|
||||
import com.commafeed.frontend.model.request.TagRequest;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import com.commafeed.security.Roles;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import io.dropwizard.hibernate.UnitOfWork;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
@@ -30,42 +30,42 @@ import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Path("/entry")
|
||||
@Path("/rest/entry")
|
||||
@RolesAllowed(Roles.USER)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Tag(name = "Feed entries")
|
||||
public class EntryREST {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedEntryTagDAO feedEntryTagDAO;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final FeedEntryTagService feedEntryTagService;
|
||||
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Mark a feed entry", description = "Mark a feed entry as read/unread")
|
||||
@Timed
|
||||
public Response markEntry(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) {
|
||||
public Response markEntry(@Valid @Parameter(description = "Mark Request", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryService.markEntry(user, Long.valueOf(req.getId()), req.isRead());
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/markMultiple")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Mark multiple feed entries", description = "Mark feed entries as read/unread")
|
||||
@Timed
|
||||
public Response markEntries(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) {
|
||||
public Response markEntries(@Valid @Parameter(description = "Multiple Mark Request", required = true) MultipleMarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getRequests());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
for (MarkRequest r : req.getRequests()) {
|
||||
Preconditions.checkNotNull(r.getId());
|
||||
feedEntryService.markEntry(user, Long.valueOf(r.getId()), r.isRead());
|
||||
@@ -76,15 +76,14 @@ public class EntryREST {
|
||||
|
||||
@Path("/star")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Star a feed entry", description = "Mark a feed entry as read/unread")
|
||||
@Timed
|
||||
public Response starEntry(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
|
||||
public Response starEntry(@Valid @Parameter(description = "Star Request", required = true) StarRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
Preconditions.checkNotNull(req.getFeedId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryService.starEntry(user, Long.valueOf(req.getId()), req.getFeedId(), req.isStarred());
|
||||
|
||||
return Response.ok().build();
|
||||
@@ -92,24 +91,23 @@ public class EntryREST {
|
||||
|
||||
@Path("/tags")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Get list of tags for the user", description = "Get list of tags for the user")
|
||||
@Timed
|
||||
public Response getTags(@Parameter(hidden = true) @SecurityCheck User user) {
|
||||
public Response getTags() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
List<String> tags = feedEntryTagDAO.findByUser(user);
|
||||
return Response.ok(tags).build();
|
||||
}
|
||||
|
||||
@Path("/tag")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Set feed entry tags")
|
||||
@Timed
|
||||
public Response tagEntry(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) {
|
||||
public Response tagEntry(@Valid @Parameter(description = "Tag Request", required = true) TagRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getEntryId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedEntryTagService.updateTags(user, req.getEntryId(), req.getTags());
|
||||
|
||||
return Response.ok().build();
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
@@ -13,9 +11,8 @@ import java.util.Objects;
|
||||
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.jboss.resteasy.reactive.RestForm;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.commafeed.CommaFeedApplication;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
@@ -44,7 +41,6 @@ import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterEx
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
import com.commafeed.backend.service.FeedService;
|
||||
import com.commafeed.backend.service.FeedSubscriptionService;
|
||||
import com.commafeed.frontend.auth.SecurityCheck;
|
||||
import com.commafeed.frontend.model.Entries;
|
||||
import com.commafeed.frontend.model.Entry;
|
||||
import com.commafeed.frontend.model.FeedInfo;
|
||||
@@ -55,6 +51,8 @@ import com.commafeed.frontend.model.request.FeedModificationRequest;
|
||||
import com.commafeed.frontend.model.request.IDRequest;
|
||||
import com.commafeed.frontend.model.request.MarkRequest;
|
||||
import com.commafeed.frontend.model.request.SubscribeRequest;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import com.commafeed.security.Roles;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.rometools.opml.feed.opml.Opml;
|
||||
@@ -63,15 +61,15 @@ import com.rometools.rome.feed.synd.SyndFeedImpl;
|
||||
import com.rometools.rome.io.SyndFeedOutput;
|
||||
import com.rometools.rome.io.WireFeedOutput;
|
||||
|
||||
import io.dropwizard.hibernate.UnitOfWork;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
@@ -90,17 +88,19 @@ import jakarta.ws.rs.core.Response.Status;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Path("/feed")
|
||||
@Path("/rest/feed")
|
||||
@RolesAllowed(Roles.USER)
|
||||
@Slf4j
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Tag(name = "Feeds")
|
||||
public class FeedREST {
|
||||
|
||||
private static final FeedEntry TEST_ENTRY = initTestEntry();
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
@@ -129,14 +129,12 @@ public class FeedREST {
|
||||
|
||||
@Path("/entries")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get feed entries",
|
||||
description = "Get a list of feed entries",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Entries.class))) })
|
||||
@Timed
|
||||
public Response getFeedEntries(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
|
||||
@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
public Response getFeedEntries(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
@Parameter(
|
||||
description = "all entries or only unread ones",
|
||||
required = true) @DefaultValue("unread") @QueryParam("readType") ReadingMode readType,
|
||||
@@ -164,6 +162,7 @@ public class FeedREST {
|
||||
|
||||
Instant newerThanDate = newerThan == null ? null : Instant.ofEpochMilli(newerThan);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(id));
|
||||
if (subscription != null) {
|
||||
entries.setName(subscription.getTitle());
|
||||
@@ -175,7 +174,7 @@ public class FeedREST {
|
||||
entryKeywords, newerThanDate, offset, limit + 1, order, true, null, null, null);
|
||||
|
||||
for (FeedEntryStatus status : list) {
|
||||
entries.getEntries().add(Entry.build(status, config.getApplicationSettings().getImageProxyEnabled()));
|
||||
entries.getEntries().add(Entry.build(status, config.imageProxyEnabled()));
|
||||
}
|
||||
|
||||
boolean hasMore = entries.getEntries().size() > limit;
|
||||
@@ -195,12 +194,10 @@ public class FeedREST {
|
||||
|
||||
@Path("/entriesAsFeed")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Get feed entries as a feed", description = "Get a feed of feed entries")
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
@Timed
|
||||
public Response getFeedEntriesAsFeed(@Parameter(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
|
||||
@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
public Response getFeedEntriesAsFeed(@Parameter(description = "id of the feed", required = true) @QueryParam("id") String id,
|
||||
@Parameter(
|
||||
description = "all entries or only unread ones",
|
||||
required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType,
|
||||
@@ -210,7 +207,7 @@ public class FeedREST {
|
||||
@Parameter(description = "date ordering") @QueryParam("order") @DefaultValue("desc") ReadingOrder order, @Parameter(
|
||||
description = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords) {
|
||||
|
||||
Response response = getFeedEntries(user, id, readType, newerThan, offset, limit, order, keywords);
|
||||
Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
@@ -220,7 +217,7 @@ public class FeedREST {
|
||||
feed.setFeedType("rss_2.0");
|
||||
feed.setTitle("CommaFeed - " + entries.getName());
|
||||
feed.setDescription("CommaFeed - " + entries.getName());
|
||||
feed.setLink(config.getApplicationSettings().getPublicUrl());
|
||||
feed.setLink(config.publicUrl());
|
||||
feed.setEntries(entries.getEntries().stream().map(Entry::asRss).toList());
|
||||
|
||||
SyndFeedOutput output = new SyndFeedOutput();
|
||||
@@ -253,14 +250,12 @@ public class FeedREST {
|
||||
|
||||
@POST
|
||||
@Path("/fetch")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Fetch a feed",
|
||||
description = "Fetch a feed by its url",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = FeedInfo.class))) })
|
||||
@Timed
|
||||
public Response fetchFeed(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) {
|
||||
public Response fetchFeed(@Valid @Parameter(description = "feed url", required = true) FeedInfoRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getUrl());
|
||||
|
||||
@@ -279,25 +274,23 @@ public class FeedREST {
|
||||
|
||||
@Path("/refreshAll")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Queue all feeds of the user for refresh", description = "Manually add all feeds of the user to the refresh queue")
|
||||
@Timed
|
||||
public Response queueAllForRefresh(@Parameter(hidden = true) @SecurityCheck User user) {
|
||||
public Response queueAllForRefresh() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedSubscriptionService.refreshAll(user);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/refresh")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Queue a feed for refresh", description = "Manually add a feed to the refresh queue")
|
||||
@Timed
|
||||
public Response queueForRefresh(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Parameter(description = "Feed id", required = true) IDRequest req) {
|
||||
|
||||
public Response queueForRefresh(@Parameter(description = "Feed id", required = true) IDRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, req.getId());
|
||||
if (sub != null) {
|
||||
Feed feed = sub.getFeed();
|
||||
@@ -309,11 +302,9 @@ public class FeedREST {
|
||||
|
||||
@Path("/mark")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Mark feed entries", description = "Mark feed entries as read (unread is not supported)")
|
||||
@Timed
|
||||
public Response markFeedEntries(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) {
|
||||
public Response markFeedEntries(@Valid @Parameter(description = "Mark request", required = true) MarkRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@@ -322,6 +313,7 @@ public class FeedREST {
|
||||
String keywords = req.getKeywords();
|
||||
List<FeedEntryKeyword> entryKeywords = FeedEntryKeyword.fromQueryString(keywords);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, Long.valueOf(req.getId()));
|
||||
if (subscription != null) {
|
||||
feedEntryService.markSubscriptionEntries(user, Collections.singletonList(subscription), olderThan, insertedBefore,
|
||||
@@ -332,15 +324,14 @@ public class FeedREST {
|
||||
|
||||
@GET
|
||||
@Path("/get/{id}")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "get feed",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Subscription.class))) })
|
||||
@Timed
|
||||
public Response getFeed(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
|
||||
public Response getFeed(@Parameter(description = "user id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, id);
|
||||
if (sub == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
@@ -351,13 +342,12 @@ public class FeedREST {
|
||||
|
||||
@GET
|
||||
@Path("/favicon/{id}")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Fetch a feed's icon", description = "Fetch a feed's icon")
|
||||
@Timed
|
||||
public Response getFeedFavicon(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Parameter(description = "subscription id", required = true) @PathParam("id") Long id) {
|
||||
|
||||
public Response getFeedFavicon(@Parameter(description = "subscription id", required = true) @PathParam("id") Long id) {
|
||||
Preconditions.checkNotNull(id);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, id);
|
||||
if (subscription == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
@@ -382,14 +372,12 @@ public class FeedREST {
|
||||
|
||||
@POST
|
||||
@Path("/subscribe")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Subscribe to a feed",
|
||||
description = "Subscribe to a feed",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Long.class))) })
|
||||
@Timed
|
||||
public Response subscribe(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) {
|
||||
public Response subscribe(@Valid @Parameter(description = "subscription request", required = true) SubscribeRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getTitle());
|
||||
Preconditions.checkNotNull(req.getUrl());
|
||||
@@ -401,6 +389,7 @@ public class FeedREST {
|
||||
}
|
||||
|
||||
FeedInfo info = fetchFeedInternal(prependHttp(req.getUrl()));
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
long subscriptionId = feedSubscriptionService.subscribe(user, info.getUrl(), req.getTitle(), category);
|
||||
return Response.ok(subscriptionId).build();
|
||||
} catch (Exception e) {
|
||||
@@ -413,19 +402,18 @@ public class FeedREST {
|
||||
|
||||
@GET
|
||||
@Path("/subscribe")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Subscribe to a feed", description = "Subscribe to a feed")
|
||||
@Timed
|
||||
public Response subscribeFromUrl(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Parameter(description = "feed url", required = true) @QueryParam("url") String url) {
|
||||
public Response subscribeFromUrl(@Parameter(description = "feed url", required = true) @QueryParam("url") String url) {
|
||||
try {
|
||||
Preconditions.checkNotNull(url);
|
||||
FeedInfo info = fetchFeedInternal(prependHttp(url));
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
feedSubscriptionService.subscribe(user, info.getUrl(), info.getTitle());
|
||||
} catch (Exception e) {
|
||||
log.info("Could not subscribe to url {} : {}", url, e.getMessage());
|
||||
}
|
||||
return Response.temporaryRedirect(URI.create(config.getApplicationSettings().getPublicUrl())).build();
|
||||
return Response.temporaryRedirect(URI.create(config.publicUrl())).build();
|
||||
}
|
||||
|
||||
private String prependHttp(String url) {
|
||||
@@ -437,13 +425,13 @@ public class FeedREST {
|
||||
|
||||
@POST
|
||||
@Path("/unsubscribe")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Unsubscribe from a feed", description = "Unsubscribe from a feed")
|
||||
@Timed
|
||||
public Response unsubscribe(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) IDRequest req) {
|
||||
public Response unsubscribe(@Parameter(required = true) IDRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
boolean deleted = feedSubscriptionService.unsubscribe(user, req.getId());
|
||||
if (deleted) {
|
||||
return Response.ok().build();
|
||||
@@ -454,11 +442,9 @@ public class FeedREST {
|
||||
|
||||
@POST
|
||||
@Path("/modify")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Modify a subscription", description = "Modify a feed subscription")
|
||||
@Timed
|
||||
public Response modifyFeed(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) {
|
||||
public Response modifyFeed(@Valid @Parameter(description = "subscription id", required = true) FeedModificationRequest req) {
|
||||
Preconditions.checkNotNull(req);
|
||||
Preconditions.checkNotNull(req.getId());
|
||||
|
||||
@@ -468,6 +454,7 @@ public class FeedREST {
|
||||
return Response.status(Status.BAD_REQUEST).entity(e.getCause().getMessage()).type(MediaType.TEXT_PLAIN).build();
|
||||
}
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
FeedSubscription subscription = feedSubscriptionDAO.findById(user, req.getId());
|
||||
subscription.setFilter(StringUtils.lowerCase(req.getFilter()));
|
||||
|
||||
@@ -509,39 +496,36 @@ public class FeedREST {
|
||||
|
||||
@POST
|
||||
@Path("/import")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(summary = "OPML import", description = "Import an OPML file, posted as a FORM with the 'file' name")
|
||||
@Timed
|
||||
public Response importOpml(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Parameter(description = "ompl file", required = true) @FormDataParam("file") InputStream input) {
|
||||
public Response importOpml(@Parameter(description = "ompl file", required = true) @RestForm("file") String opml) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN).entity("Import is disabled for the demo account").build();
|
||||
}
|
||||
try {
|
||||
String opml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
opmlImporter.importOpml(user, opml);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build());
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/export")
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
@Operation(summary = "OPML export", description = "Export an OPML file of the user's subscriptions")
|
||||
@Timed
|
||||
public Response exportOpml(@Parameter(hidden = true) @SecurityCheck User user) {
|
||||
public Response exportOpml() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
Opml opml = opmlExporter.export(user);
|
||||
WireFeedOutput output = new WireFeedOutput();
|
||||
String opmlString;
|
||||
try {
|
||||
opmlString = output.outputString(opml);
|
||||
} catch (Exception e) {
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e).build();
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
|
||||
}
|
||||
return Response.ok(opmlString).build();
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
package com.commafeed.frontend.resource;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedVersion;
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.frontend.auth.SecurityCheck;
|
||||
import com.commafeed.frontend.model.ServerInfo;
|
||||
import com.commafeed.security.Roles;
|
||||
|
||||
import io.dropwizard.hibernate.UnitOfWork;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
@@ -30,49 +28,50 @@ import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Path("/server")
|
||||
@Path("/rest/server")
|
||||
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Tag(name = "Server")
|
||||
public class ServerREST {
|
||||
|
||||
private final HttpGetter httpGetter;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final CommaFeedVersion version;
|
||||
|
||||
@Path("/get")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@PermitAll
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Get server infos",
|
||||
description = "Get server infos",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = ServerInfo.class))) })
|
||||
@Timed
|
||||
public Response getServerInfos() {
|
||||
ServerInfo infos = new ServerInfo();
|
||||
infos.setAnnouncement(config.getApplicationSettings().getAnnouncement());
|
||||
infos.setVersion(config.getVersion());
|
||||
infos.setGitCommit(config.getGitCommit());
|
||||
infos.setAllowRegistrations(config.getApplicationSettings().getAllowRegistrations());
|
||||
infos.setGoogleAnalyticsCode(config.getApplicationSettings().getGoogleAnalyticsTrackingCode());
|
||||
infos.setSmtpEnabled(StringUtils.isNotBlank(config.getApplicationSettings().getSmtpHost()));
|
||||
infos.setDemoAccountEnabled(config.getApplicationSettings().getCreateDemoAccount());
|
||||
infos.setWebsocketEnabled(config.getApplicationSettings().getWebsocketEnabled());
|
||||
infos.setWebsocketPingInterval(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds());
|
||||
infos.setTreeReloadInterval(config.getApplicationSettings().getTreeReloadInterval().toMilliseconds());
|
||||
infos.setAnnouncement(config.announcement().orElse(null));
|
||||
infos.setVersion(version.getVersion());
|
||||
infos.setGitCommit(version.getGitCommit());
|
||||
infos.setAllowRegistrations(config.users().allowRegistrations());
|
||||
infos.setGoogleAnalyticsCode(config.googleAnalyticsTrackingCode().orElse(null));
|
||||
infos.setSmtpEnabled(config.smtp().isPresent());
|
||||
infos.setDemoAccountEnabled(config.users().createDemoAccount());
|
||||
infos.setWebsocketEnabled(config.websocket().enabled());
|
||||
infos.setWebsocketPingInterval(config.websocket().pingInterval().toMillis());
|
||||
infos.setTreeReloadInterval(config.websocket().treeReloadInterval().toMillis());
|
||||
return Response.ok(infos).build();
|
||||
}
|
||||
|
||||
@Path("/proxy")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@RolesAllowed(Roles.USER)
|
||||
@Transactional
|
||||
@Operation(summary = "proxy image")
|
||||
@Produces("image/png")
|
||||
@Timed
|
||||
public Response getProxiedImage(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Parameter(description = "image url", required = true) @QueryParam("u") String url) {
|
||||
if (!config.getApplicationSettings().getImageProxyEnabled()) {
|
||||
public Response getProxiedImage(@Parameter(description = "image url", required = true) @QueryParam("u") String url) {
|
||||
if (!config.imageProxyEnabled()) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hc.core5.net.URIBuilder;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.commafeed.CommaFeedApplication;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.Digests;
|
||||
@@ -29,25 +28,25 @@ 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.frontend.auth.SecurityCheck;
|
||||
import com.commafeed.frontend.model.Settings;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
import com.commafeed.frontend.model.request.LoginRequest;
|
||||
import com.commafeed.frontend.model.request.PasswordResetRequest;
|
||||
import com.commafeed.frontend.model.request.ProfileModificationRequest;
|
||||
import com.commafeed.frontend.model.request.RegistrationRequest;
|
||||
import com.commafeed.frontend.session.SessionHelper;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import com.commafeed.security.Roles;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import io.dropwizard.hibernate.UnitOfWork;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
@@ -56,22 +55,23 @@ import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Path("/user")
|
||||
@Path("/rest/user")
|
||||
@RolesAllowed(Roles.USER)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Tag(name = "Users")
|
||||
public class UserREST {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserDAO userDAO;
|
||||
private final UserRoleDAO userRoleDAO;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
@@ -82,14 +82,15 @@ public class UserREST {
|
||||
|
||||
@Path("/settings")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Retrieve user settings",
|
||||
description = "Retrieve user settings",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = Settings.class))) })
|
||||
@Timed
|
||||
public Response getUserSettings(@Parameter(hidden = true) @SecurityCheck User user) {
|
||||
public Response getUserSettings() {
|
||||
Settings s = new Settings();
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
if (settings != null) {
|
||||
s.setReadingMode(settings.getReadingMode().name());
|
||||
@@ -145,12 +146,12 @@ public class UserREST {
|
||||
|
||||
@Path("/settings")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Save user settings", description = "Save user settings")
|
||||
@Timed
|
||||
public Response saveUserSettings(@Parameter(hidden = true) @SecurityCheck User user, @Parameter(required = true) Settings settings) {
|
||||
public Response saveUserSettings(@Parameter(required = true) Settings settings) {
|
||||
Preconditions.checkNotNull(settings);
|
||||
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
UserSettings s = userSettingsDAO.findByUser(user);
|
||||
if (s == null) {
|
||||
s = new UserSettings();
|
||||
@@ -187,12 +188,13 @@ public class UserREST {
|
||||
|
||||
@Path("/profile")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(
|
||||
summary = "Retrieve user's profile",
|
||||
responses = { @ApiResponse(content = @Content(schema = @Schema(implementation = UserModel.class))) })
|
||||
@Timed
|
||||
public Response getUserProfile(@Parameter(hidden = true) @SecurityCheck User user) {
|
||||
public Response getUserProfile() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
|
||||
UserModel userModel = new UserModel();
|
||||
userModel.setId(user.getId());
|
||||
userModel.setName(user.getName());
|
||||
@@ -209,11 +211,10 @@ public class UserREST {
|
||||
|
||||
@Path("/profile")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Save user's profile")
|
||||
@Timed
|
||||
public Response saveUserProfile(@Parameter(hidden = true) @SecurityCheck User user,
|
||||
@Valid @Parameter(required = true) ProfileModificationRequest request) {
|
||||
public Response saveUserProfile(@Valid @Parameter(required = true) ProfileModificationRequest request) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
@@ -242,49 +243,29 @@ public class UserREST {
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
|
||||
userDAO.update(user);
|
||||
userDAO.saveOrUpdate(user);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@Path("/register")
|
||||
@PermitAll
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Register a new account")
|
||||
@Timed
|
||||
public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req,
|
||||
@Context @Parameter(hidden = true) SessionHelper sessionHelper) {
|
||||
public Response registerUser(@Valid @Parameter(required = true) RegistrationRequest req) {
|
||||
try {
|
||||
User registeredUser = userService.register(req.getName(), req.getPassword(), req.getEmail(),
|
||||
Collections.singletonList(Role.USER));
|
||||
userService.login(req.getName(), req.getPassword());
|
||||
sessionHelper.setLoggedInUser(registeredUser);
|
||||
userService.register(req.getName(), req.getPassword(), req.getEmail(), Collections.singletonList(Role.USER));
|
||||
return Response.ok().build();
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new BadRequestException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/login")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Operation(summary = "Login and create a session")
|
||||
@Timed
|
||||
public Response login(@Valid @Parameter(required = true) LoginRequest req,
|
||||
@Parameter(hidden = true) @Context SessionHelper sessionHelper) {
|
||||
Optional<User> user = userService.login(req.getName(), req.getPassword());
|
||||
if (user.isPresent()) {
|
||||
sessionHelper.setLoggedInUser(user.get());
|
||||
return Response.ok().build();
|
||||
} else {
|
||||
return Response.status(Response.Status.UNAUTHORIZED).entity("wrong username or password").type(MediaType.TEXT_PLAIN).build();
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/passwordReset")
|
||||
@PermitAll
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "send a password reset email")
|
||||
@Timed
|
||||
public Response sendPasswordReset(@Valid @Parameter(required = true) PasswordResetRequest req) {
|
||||
User user = userDAO.findByEmail(req.getEmail());
|
||||
if (user == null) {
|
||||
@@ -304,7 +285,7 @@ public class UserREST {
|
||||
}
|
||||
|
||||
private String buildEmailContent(User user) throws Exception {
|
||||
String publicUrl = FeedUtils.removeTrailingSlash(config.getApplicationSettings().getPublicUrl());
|
||||
String publicUrl = FeedUtils.removeTrailingSlash(config.publicUrl());
|
||||
publicUrl += "/rest/user/passwordResetCallback";
|
||||
return String.format(
|
||||
"You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
|
||||
@@ -320,10 +301,10 @@ public class UserREST {
|
||||
}
|
||||
|
||||
@Path("/passwordResetCallback")
|
||||
@PermitAll
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
@Timed
|
||||
public Response passwordRecoveryCallback(@Parameter(required = true) @QueryParam("email") String email,
|
||||
@Parameter(required = true) @QueryParam("token") String token) {
|
||||
Preconditions.checkNotNull(email);
|
||||
@@ -352,16 +333,16 @@ public class UserREST {
|
||||
|
||||
String message = "Your new password is: " + passwd;
|
||||
message += "<br />";
|
||||
message += String.format("<a href=\"%s\">Back to Homepage</a>", config.getApplicationSettings().getPublicUrl());
|
||||
message += String.format("<a href=\"%s\">Back to Homepage</a>", config.publicUrl());
|
||||
return Response.ok(message).build();
|
||||
}
|
||||
|
||||
@Path("/profile/deleteAccount")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Transactional
|
||||
@Operation(summary = "Delete the user account")
|
||||
@Timed
|
||||
public Response deleteUser(@Parameter(hidden = true) @SecurityCheck User user) {
|
||||
public Response deleteUser() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (CommaFeedApplication.USERNAME_ADMIN.equals(user.getName()) || CommaFeedApplication.USERNAME_DEMO.equals(user.getName())) {
|
||||
return Response.status(Status.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
|
||||
import org.jboss.resteasy.reactive.server.multipart.FormValue;
|
||||
import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
@@ -37,10 +37,10 @@ import com.commafeed.frontend.resource.fever.FeverResponse.FeverFeedGroup;
|
||||
import com.commafeed.frontend.resource.fever.FeverResponse.FeverGroup;
|
||||
import com.commafeed.frontend.resource.fever.FeverResponse.FeverItem;
|
||||
|
||||
import io.dropwizard.hibernate.UnitOfWork;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
@@ -64,9 +64,10 @@ import lombok.RequiredArgsConstructor;
|
||||
*
|
||||
* See https://feedafever.com/api
|
||||
*/
|
||||
@Path("/fever")
|
||||
@Path("/rest/fever")
|
||||
@PermitAll
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
@Hidden
|
||||
public class FeverREST {
|
||||
@@ -88,8 +89,7 @@ public class FeverREST {
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Timed
|
||||
@Transactional
|
||||
public FeverResponse formUrlencoded(@Context UriInfo uri, @PathParam("userId") Long userId, MultivaluedMap<String, String> form) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
|
||||
@@ -101,8 +101,7 @@ public class FeverREST {
|
||||
// e.g. FeedMe
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Timed
|
||||
@Transactional
|
||||
public FeverResponse noForm(@Context UriInfo uri, @PathParam("userId") Long userId) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
|
||||
@@ -113,8 +112,7 @@ public class FeverREST {
|
||||
// e.g. Unread
|
||||
@Path(PATH)
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Timed
|
||||
@Transactional
|
||||
public FeverResponse get(@Context UriInfo uri, @PathParam("userId") Long userId) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
|
||||
@@ -126,12 +124,11 @@ public class FeverREST {
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Path(PATH)
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@Timed
|
||||
public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, FormDataMultiPart form) {
|
||||
@Transactional
|
||||
public FeverResponse formData(@Context UriInfo uri, @PathParam("userId") Long userId, MultipartFormDataInput form) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.get(0)));
|
||||
form.getFields().forEach((k, v) -> params.put(k, v.get(0).getValue()));
|
||||
form.getValues().forEach((k, v) -> params.put(k, v.stream().map(FormValue::getValue).findFirst().orElse(null)));
|
||||
return handle(userId, params);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.frontend.session.SessionHelper;
|
||||
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
abstract class AbstractCustomCodeServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final transient UnitOfWork unitOfWork;
|
||||
private final transient UserDAO userDAO;
|
||||
private final transient UserSettingsDAO userSettingsDAO;
|
||||
|
||||
@Override
|
||||
protected final void doGet(final HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||
resp.setContentType(getMimeType());
|
||||
|
||||
SessionHelper sessionHelper = new SessionHelper(req);
|
||||
Optional<Long> userId = sessionHelper.getLoggedInUserId();
|
||||
final Optional<User> user = unitOfWork.call(() -> userId.map(userDAO::findById));
|
||||
if (user.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user.get()));
|
||||
if (settings == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String customCode = getCustomCode(settings);
|
||||
if (customCode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
resp.getWriter().write(customCode);
|
||||
}
|
||||
|
||||
protected abstract String getMimeType();
|
||||
|
||||
protected abstract String getCustomCode(UserSettings settings);
|
||||
}
|
||||
@@ -1,28 +1,39 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public class CustomCssServlet extends AbstractCustomCodeServlet {
|
||||
@Path("/custom_css.css")
|
||||
@Produces("text/css")
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class CustomCssServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final UnitOfWork unitOfWork;
|
||||
|
||||
@Inject
|
||||
public CustomCssServlet(UnitOfWork unitOfWork, UserDAO userDAO, UserSettingsDAO userSettingsDAO) {
|
||||
super(unitOfWork, userDAO, userSettingsDAO);
|
||||
}
|
||||
@GET
|
||||
public String get() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getMimeType() {
|
||||
return "text/css";
|
||||
}
|
||||
UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user));
|
||||
if (settings == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCustomCode(UserSettings settings) {
|
||||
return settings.getCustomCss();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Path("/custom_js.js")
|
||||
@Produces("application/javascript")
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class CustomJsServlet extends AbstractCustomCodeServlet {
|
||||
public class CustomJsServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final UnitOfWork unitOfWork;
|
||||
|
||||
@Inject
|
||||
public CustomJsServlet(UnitOfWork unitOfWork, UserDAO userDAO, UserSettingsDAO userSettingsDAO) {
|
||||
super(unitOfWork, userDAO, userSettingsDAO);
|
||||
}
|
||||
@GET
|
||||
public String get() {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getMimeType() {
|
||||
return "application/javascript";
|
||||
}
|
||||
UserSettings settings = unitOfWork.call(() -> userSettingsDAO.findByUser(user));
|
||||
if (settings == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCustomCode(UserSettings settings) {
|
||||
return settings.getCustomJs();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.core.NewCookie;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@Path("/logout")
|
||||
@PermitAll
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class LogoutServlet extends HttpServlet {
|
||||
public class LogoutServlet {
|
||||
|
||||
@ConfigProperty(name = "quarkus.http.auth.form.cookie-name")
|
||||
String cookieName;
|
||||
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||
req.getSession().invalidate();
|
||||
resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl()));
|
||||
@GET
|
||||
public Response get() {
|
||||
NewCookie removeCookie = new NewCookie.Builder(cookieName).maxAge(0).expiry(Date.from(Instant.EPOCH)).path("/").build();
|
||||
return Response.temporaryRedirect(URI.create(config.publicUrl())).cookie(removeCookie).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -11,86 +10,68 @@ import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.frontend.resource.CategoryREST;
|
||||
import com.commafeed.frontend.session.SessionHelper;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@Path("/next")
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class NextUnreadServlet extends HttpServlet {
|
||||
|
||||
private static final String PARAM_CATEGORYID = "category";
|
||||
private static final String PARAM_READINGORDER = "order";
|
||||
public class NextUnreadServlet {
|
||||
|
||||
private final UnitOfWork unitOfWork;
|
||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final FeedCategoryDAO feedCategoryDAO;
|
||||
private final UserDAO userDAO;
|
||||
private final UserService userService;
|
||||
private final FeedEntryService feedEntryService;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final AuthenticationContext authenticationContext;
|
||||
|
||||
@Override
|
||||
protected void doGet(final HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||
final String categoryId = req.getParameter(PARAM_CATEGORYID);
|
||||
String orderParam = req.getParameter(PARAM_READINGORDER);
|
||||
|
||||
SessionHelper sessionHelper = new SessionHelper(req);
|
||||
Optional<Long> userId = sessionHelper.getLoggedInUserId();
|
||||
Optional<User> user = unitOfWork.call(() -> userId.map(userDAO::findById));
|
||||
user.ifPresent(value -> unitOfWork.run(() -> userService.performPostLoginActivities(value)));
|
||||
if (user.isEmpty()) {
|
||||
resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl()));
|
||||
return;
|
||||
@GET
|
||||
public Response get(@QueryParam("category") String categoryId, @QueryParam("order") @DefaultValue("desc") ReadingOrder order) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return Response.temporaryRedirect(URI.create(config.publicUrl())).build();
|
||||
}
|
||||
|
||||
final ReadingOrder order = StringUtils.equals(orderParam, "asc") ? ReadingOrder.asc : ReadingOrder.desc;
|
||||
|
||||
FeedEntryStatus status = unitOfWork.call(() -> {
|
||||
FeedEntryStatus s = null;
|
||||
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user.get());
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subs, true, null, null, 0, 1, order,
|
||||
true, null, null, null);
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true,
|
||||
null, null, null);
|
||||
s = Iterables.getFirst(statuses, null);
|
||||
} else {
|
||||
FeedCategory category = feedCategoryDAO.findById(user.get(), Long.valueOf(categoryId));
|
||||
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
|
||||
if (category != null) {
|
||||
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user.get(), category);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user.get(), children);
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user.get(), subscriptions, true, null, null, 0,
|
||||
1, order, true, null, null, null);
|
||||
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1,
|
||||
order, true, null, null, null);
|
||||
s = Iterables.getFirst(statuses, null);
|
||||
}
|
||||
}
|
||||
if (s != null) {
|
||||
feedEntryService.markEntry(user.get(), s.getEntry().getId(), true);
|
||||
feedEntryService.markEntry(user, s.getEntry().getId(), true);
|
||||
}
|
||||
return s;
|
||||
});
|
||||
|
||||
if (status == null) {
|
||||
resp.sendRedirect(resp.encodeRedirectURL(config.getApplicationSettings().getPublicUrl()));
|
||||
} else {
|
||||
String url = status.getEntry().getUrl();
|
||||
resp.sendRedirect(resp.encodeRedirectURL(url));
|
||||
}
|
||||
String url = status == null ? config.publicUrl() : status.getEntry().getUrl();
|
||||
return Response.temporaryRedirect(URI.create(url)).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
package com.commafeed.frontend.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Path("/robots.txt")
|
||||
@PermitAll
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@RequiredArgsConstructor
|
||||
@Singleton
|
||||
public class RobotsTxtDisallowAllServlet extends HttpServlet {
|
||||
public class RobotsTxtDisallowAllServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().write("User-agent: *");
|
||||
resp.getWriter().write("\n");
|
||||
resp.getWriter().write("Disallow: /");
|
||||
@GET
|
||||
public Response get() {
|
||||
if (config.hideFromWebCrawlers()) {
|
||||
return Response.ok("User-agent: *\nDisallow: /").build();
|
||||
} else {
|
||||
return Response.status(HttpStatus.SC_NOT_FOUND).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.commafeed.frontend.session;
|
||||
|
||||
import org.eclipse.jetty.server.session.DatabaseAdaptor;
|
||||
import org.eclipse.jetty.server.session.DefaultSessionCache;
|
||||
import org.eclipse.jetty.server.session.JDBCSessionDataStore;
|
||||
import org.eclipse.jetty.server.session.SessionCache;
|
||||
import org.eclipse.jetty.server.session.SessionHandler;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.util.Duration;
|
||||
import jakarta.servlet.SessionTrackingMode;
|
||||
|
||||
public class SessionHandlerFactory {
|
||||
|
||||
@JsonProperty
|
||||
private Duration cookieMaxAge = Duration.days(30);
|
||||
|
||||
@JsonProperty
|
||||
private Duration cookieRefreshAge = Duration.days(1);
|
||||
|
||||
@JsonProperty
|
||||
private Duration maxInactiveInterval = Duration.days(30);
|
||||
|
||||
@JsonProperty
|
||||
private Duration savePeriod = Duration.minutes(5);
|
||||
|
||||
public SessionHandler build(DataSourceFactory dataSourceFactory) {
|
||||
SessionHandler sessionHandler = new SessionHandler();
|
||||
sessionHandler.setHttpOnly(true);
|
||||
sessionHandler.setSessionTrackingModes(ImmutableSet.of(SessionTrackingMode.COOKIE));
|
||||
sessionHandler.setMaxInactiveInterval((int) maxInactiveInterval.toSeconds());
|
||||
sessionHandler.setRefreshCookieAge((int) cookieRefreshAge.toSeconds());
|
||||
sessionHandler.getSessionCookieConfig().setMaxAge((int) cookieMaxAge.toSeconds());
|
||||
|
||||
SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
|
||||
sessionHandler.setSessionCache(sessionCache);
|
||||
|
||||
JDBCSessionDataStore dataStore = new JDBCSessionDataStore();
|
||||
dataStore.setSavePeriodSec((int) savePeriod.toSeconds());
|
||||
sessionCache.setSessionDataStore(dataStore);
|
||||
|
||||
DatabaseAdaptor adaptor = new DatabaseAdaptor();
|
||||
adaptor.setDatasource(dataSourceFactory.build(new MetricRegistry(), "sessions"));
|
||||
dataStore.setDatabaseAdaptor(adaptor);
|
||||
|
||||
return sessionHandler;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.commafeed.frontend.session;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class SessionHelper {
|
||||
|
||||
public static final String SESSION_KEY_USER_ID = "user-id";
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
public Optional<Long> getLoggedInUserId() {
|
||||
HttpSession session = request.getSession(false);
|
||||
return getLoggedInUserId(session);
|
||||
}
|
||||
|
||||
public static Optional<Long> getLoggedInUserId(HttpSession session) {
|
||||
if (session == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Long userId = (Long) session.getAttribute(SESSION_KEY_USER_ID);
|
||||
return Optional.ofNullable(userId);
|
||||
}
|
||||
|
||||
public void setLoggedInUser(User user) {
|
||||
request.getSession(true).setAttribute(SESSION_KEY_USER_ID, user.getId());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.commafeed.frontend.session;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.glassfish.hk2.utilities.binding.AbstractBinder;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.internal.inject.AbstractValueParamProvider;
|
||||
import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
|
||||
import org.glassfish.jersey.server.model.Parameter;
|
||||
import org.glassfish.jersey.server.spi.internal.ValueParamProvider;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
|
||||
@Singleton
|
||||
public class SessionHelperFactoryProvider extends AbstractValueParamProvider {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
@Inject
|
||||
public SessionHelperFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, HttpServletRequest request) {
|
||||
super(() -> extractorProvider, Parameter.Source.CONTEXT);
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Function<ContainerRequest, ?> createValueProvider(Parameter parameter) {
|
||||
final Class<?> classType = parameter.getRawType();
|
||||
|
||||
Context context = parameter.getAnnotation(Context.class);
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!classType.isAssignableFrom(SessionHelper.class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return r -> new SessionHelper(request);
|
||||
}
|
||||
|
||||
public static class Binder extends AbstractBinder {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(SessionHelperFactoryProvider.class).to(ValueParamProvider.class).in(Singleton.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.commafeed.frontend.ws;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.commafeed.frontend.session.SessionHelper;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.websocket.HandshakeResponse;
|
||||
import jakarta.websocket.server.HandshakeRequest;
|
||||
import jakarta.websocket.server.ServerEndpointConfig;
|
||||
import jakarta.websocket.server.ServerEndpointConfig.Configurator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Singleton
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
public class WebSocketConfigurator extends Configurator {
|
||||
|
||||
public static final String SESSIONKEY_USERID = "userId";
|
||||
|
||||
private final WebSocketSessions webSocketSessions;
|
||||
|
||||
@Override
|
||||
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
|
||||
HttpSession httpSession = (HttpSession) request.getHttpSession();
|
||||
if (httpSession != null) {
|
||||
Optional<Long> userId = SessionHelper.getLoggedInUserId(httpSession);
|
||||
userId.ifPresent(value -> config.getUserProperties().put(SESSIONKEY_USERID, value));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getEndpointInstance(Class<T> endpointClass) {
|
||||
return (T) new WebSocketEndpoint(webSocketSessions);
|
||||
}
|
||||
}
|
||||
@@ -2,39 +2,54 @@ package com.commafeed.frontend.ws;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.security.AuthenticationContext;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.websocket.CloseReason;
|
||||
import jakarta.websocket.CloseReason.CloseCodes;
|
||||
import jakarta.websocket.Endpoint;
|
||||
import jakarta.websocket.EndpointConfig;
|
||||
import jakarta.websocket.OnClose;
|
||||
import jakarta.websocket.OnMessage;
|
||||
import jakarta.websocket.OnOpen;
|
||||
import jakarta.websocket.Session;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
public class WebSocketEndpoint extends Endpoint {
|
||||
@ServerEndpoint("/ws")
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketEndpoint {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final CommaFeedConfiguration config;
|
||||
private final WebSocketSessions sessions;
|
||||
|
||||
@Override
|
||||
public void onOpen(Session session, EndpointConfig config) {
|
||||
Long userId = (Long) config.getUserProperties().get(WebSocketConfigurator.SESSIONKEY_USERID);
|
||||
if (userId == null) {
|
||||
@OnOpen
|
||||
public void onOpen(Session session) {
|
||||
User user = authenticationContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
reject(session);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("created websocket session for user {}", userId);
|
||||
sessions.add(userId, session);
|
||||
log.debug("created websocket session for user '{}'", user.getName());
|
||||
sessions.add(user.getId(), session);
|
||||
session.setMaxIdleTimeout(config.websocket().pingInterval().toMillis() + 10000);
|
||||
}
|
||||
|
||||
session.addMessageHandler(String.class, message -> {
|
||||
if ("ping".equals(message)) {
|
||||
session.getAsyncRemote().sendText("pong");
|
||||
}
|
||||
});
|
||||
@OnMessage
|
||||
public void onMessage(String message, Session session) {
|
||||
if ("ping".equals(message)) {
|
||||
session.getAsyncRemote().sendText("pong");
|
||||
}
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(Session session) {
|
||||
sessions.remove(session);
|
||||
}
|
||||
|
||||
private void reject(Session session) {
|
||||
@@ -45,9 +60,4 @@ public class WebSocketEndpoint extends Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(Session session, CloseReason reason) {
|
||||
sessions.remove(session);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import jakarta.websocket.Session;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -20,7 +19,6 @@ public class WebSocketSessions {
|
||||
// a user may have multiple sessions (two tabs, two devices, ...)
|
||||
private final Map<Long, Set<Session>> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Inject
|
||||
public WebSocketSessions(MetricRegistry metrics) {
|
||||
metrics.register(MetricRegistry.name(getClass(), "users"),
|
||||
(Gauge<Long>) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count());
|
||||
|
||||
Reference in New Issue
Block a user