APIs for profile photos (#3260)
* Add API for uploading profile photos for current user * Add delete and get APIs for profile photos * Add test for uploading and deleting profile photo * Added negative tests for upload profile photo API
This commit is contained in:
parent
092a942036
commit
bb9a9a307f
|
|
@ -4,11 +4,6 @@ import com.appsmith.server.constants.Url;
|
|||
import com.appsmith.server.services.AssetService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.buffer.DefaultDataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
|
@ -26,21 +21,7 @@ public class AssetController {
|
|||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Void> getById(@PathVariable String id, ServerWebExchange exchange) {
|
||||
log.debug("Returning asset with ID '{}'.", id);
|
||||
|
||||
final ServerHttpResponse response = exchange.getResponse();
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
|
||||
final Mono<DefaultDataBuffer> imageBufferMono = service.getById(id)
|
||||
.map(asset -> {
|
||||
final String contentType = asset.getContentType();
|
||||
if (contentType != null) {
|
||||
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
}
|
||||
return new DefaultDataBufferFactory().wrap(asset.getData());
|
||||
});
|
||||
|
||||
return response.writeWith(imageBufferMono);
|
||||
return service.makeImageResponse(exchange, id);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.appsmith.server.controllers;
|
|||
|
||||
import com.appsmith.server.constants.Url;
|
||||
import com.appsmith.server.domains.User;
|
||||
import com.appsmith.server.domains.UserData;
|
||||
import com.appsmith.server.dtos.InviteUsersDTO;
|
||||
import com.appsmith.server.dtos.ResetUserPasswordDTO;
|
||||
import com.appsmith.server.dtos.ResponseDTO;
|
||||
|
|
@ -14,6 +15,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
|
@ -22,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
|
@ -142,4 +146,34 @@ public class UserController extends BaseController<UserService, User, String> {
|
|||
.thenReturn(new ResponseDTO<>(HttpStatus.OK.value(), null, null));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/photo", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public Mono<ResponseDTO<UserData>> uploadProfilePhoto(@RequestPart("file") Mono<Part> fileMono) {
|
||||
return fileMono
|
||||
.flatMap(userDataService::saveProfilePhoto)
|
||||
.map(url -> new ResponseDTO<>(HttpStatus.OK.value(), url, null));
|
||||
}
|
||||
|
||||
@DeleteMapping("/photo")
|
||||
public Mono<ResponseDTO<Void>> deleteProfilePhoto() {
|
||||
return userDataService
|
||||
.deleteProfilePhoto()
|
||||
.map(ignored -> new ResponseDTO<>(HttpStatus.OK.value(), null, null));
|
||||
}
|
||||
|
||||
@GetMapping("/photo")
|
||||
public Mono<Void> getProfilePhoto(ServerWebExchange exchange) {
|
||||
return userDataService.makeProfilePhotoResponse(exchange)
|
||||
.switchIfEmpty(Mono.fromRunnable(() -> {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
|
||||
}));
|
||||
}
|
||||
|
||||
@GetMapping("/photo/{email}")
|
||||
public Mono<Void> getProfilePhoto(ServerWebExchange exchange, @PathVariable String email) {
|
||||
return userDataService.makeProfilePhotoResponse(exchange, email)
|
||||
.switchIfEmpty(Mono.fromRunnable(() -> {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.appsmith.server.domains;
|
|||
import com.appsmith.external.models.BaseDomain;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
|
@ -14,11 +15,15 @@ import org.springframework.data.mongodb.core.mapping.Document;
|
|||
@Setter
|
||||
@ToString
|
||||
@Document
|
||||
@NoArgsConstructor
|
||||
public class UserData extends BaseDomain {
|
||||
|
||||
@JsonIgnore
|
||||
String userId;
|
||||
|
||||
// The ID of the asset which has the profile photo of this user.
|
||||
private String profilePhotoAssetId;
|
||||
|
||||
// The version where this user has last viewed the release notes.
|
||||
private String releaseNotesViewedVersion;
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,12 @@ public class AnalyticsService {
|
|||
TrackMessage.Builder messageBuilder = TrackMessage.builder(event).userId(userId);
|
||||
|
||||
if (!CollectionUtils.isEmpty(properties)) {
|
||||
// Segment throws an NPE if any value in `properties` is null.
|
||||
for (final Map.Entry<String, Object> entry : properties.entrySet()) {
|
||||
if (entry.getValue() == null) {
|
||||
properties.put(entry.getKey(), "");
|
||||
}
|
||||
}
|
||||
messageBuilder = messageBuilder.properties(properties);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
package com.appsmith.server.services;
|
||||
|
||||
import com.appsmith.server.domains.Asset;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface AssetService {
|
||||
|
||||
Mono<Asset> getById(String id);
|
||||
|
||||
Mono<Asset> upload(Part filePart, int i);
|
||||
|
||||
Mono<Void> remove(String assetId);
|
||||
|
||||
Mono<Void> makeImageResponse(ServerWebExchange exchange, String assetId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,26 @@
|
|||
package com.appsmith.server.services;
|
||||
|
||||
import com.appsmith.server.domains.Asset;
|
||||
import com.appsmith.server.exceptions.AppsmithError;
|
||||
import com.appsmith.server.exceptions.AppsmithException;
|
||||
import com.appsmith.server.repositories.AssetRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -14,9 +28,84 @@ public class AssetServiceImpl implements AssetService {
|
|||
|
||||
private final AssetRepository repository;
|
||||
|
||||
private final AnalyticsService analyticsService;
|
||||
|
||||
private static final Set<MediaType> ALLOWED_CONTENT_TYPES = Set.of(MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG);
|
||||
|
||||
@Override
|
||||
public Mono<Asset> getById(String id) {
|
||||
return repository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Asset> upload(Part filePart, int maxFileSizeKB) {
|
||||
if (filePart == null) {
|
||||
return Mono.error(new AppsmithException(AppsmithError.VALIDATION_FAILURE, "Please upload a valid image."));
|
||||
}
|
||||
|
||||
// The reason we restrict file types here is to avoid having to deal with dangerous image types such as SVG,
|
||||
// which can have arbitrary HTML/JS inside of them.
|
||||
final MediaType contentType = filePart.headers().getContentType();
|
||||
if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) {
|
||||
return Mono.error(new AppsmithException(
|
||||
AppsmithError.VALIDATION_FAILURE,
|
||||
"Please upload a valid image. Only JPEG and PNG are allowed."
|
||||
));
|
||||
}
|
||||
|
||||
final Flux<DataBuffer> contentCache = filePart.content().cache();
|
||||
|
||||
return contentCache.count()
|
||||
.defaultIfEmpty(0L)
|
||||
.flatMap(count -> {
|
||||
// Default implementation for the BufferFactory used breaks down the FilePart into chunks of 4KB.
|
||||
// So we multiply the count of chunks with 4 to get an estimate on the file size in KB.
|
||||
if (4 * count > maxFileSizeKB) {
|
||||
return Mono.error(new AppsmithException(AppsmithError.PAYLOAD_TOO_LARGE, maxFileSizeKB));
|
||||
}
|
||||
return DataBufferUtils.join(contentCache);
|
||||
})
|
||||
.flatMap(dataBuffer -> {
|
||||
byte[] data = new byte[dataBuffer.readableByteCount()];
|
||||
dataBuffer.read(data);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
return repository.save(new Asset(contentType, data));
|
||||
})
|
||||
.flatMap(analyticsService::sendCreateEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function hard-deletes (read: not archive) the asset given by the ID. It is intended to be used to delete an
|
||||
* old asset when a user uploads a new one. For example, when a new profile photo or an organization logo is,
|
||||
* uploaded, this method is used to completely delete the old one, if any.
|
||||
* @param assetId The ID string of the asset to delete.
|
||||
* @return empty Mono
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> remove(String assetId) {
|
||||
final Asset tempAsset = new Asset();
|
||||
tempAsset.setId(assetId);
|
||||
return repository.deleteById(assetId)
|
||||
.then(analyticsService.sendDeleteEvent(tempAsset))
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> makeImageResponse(ServerWebExchange exchange, String assetId) {
|
||||
log.debug("Returning asset with ID '{}'.", assetId);
|
||||
return getById(assetId)
|
||||
.flatMap(asset -> {
|
||||
final String contentType = asset.getContentType();
|
||||
final ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
|
||||
if (contentType != null) {
|
||||
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
}
|
||||
|
||||
return response.writeWith(Mono.just(new DefaultDataBufferFactory().wrap(asset.getData())));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import com.appsmith.server.repositories.PluginRepository;
|
|||
import com.appsmith.server.repositories.UserRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
|
||||
import org.springframework.data.mongodb.core.convert.MongoConverter;
|
||||
|
|
@ -28,6 +26,7 @@ import org.springframework.http.codec.multipart.Part;
|
|||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
|
@ -55,6 +54,7 @@ public class OrganizationServiceImpl extends BaseService<OrganizationRepository,
|
|||
private final UserRepository userRepository;
|
||||
private final RoleGraph roleGraph;
|
||||
private final AssetRepository assetRepository;
|
||||
private final AssetService assetService;
|
||||
|
||||
@Autowired
|
||||
public OrganizationServiceImpl(Scheduler scheduler,
|
||||
|
|
@ -68,7 +68,8 @@ public class OrganizationServiceImpl extends BaseService<OrganizationRepository,
|
|||
UserOrganizationService userOrganizationService,
|
||||
UserRepository userRepository,
|
||||
RoleGraph roleGraph,
|
||||
AssetRepository assetRepository) {
|
||||
AssetRepository assetRepository,
|
||||
AssetService assetService) {
|
||||
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
|
||||
this.pluginRepository = pluginRepository;
|
||||
this.sessionUserService = sessionUserService;
|
||||
|
|
@ -76,6 +77,7 @@ public class OrganizationServiceImpl extends BaseService<OrganizationRepository,
|
|||
this.userRepository = userRepository;
|
||||
this.roleGraph = roleGraph;
|
||||
this.assetRepository = assetRepository;
|
||||
this.assetService = assetService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -290,50 +292,26 @@ public class OrganizationServiceImpl extends BaseService<OrganizationRepository,
|
|||
|
||||
@Override
|
||||
public Mono<Organization> uploadLogo(String organizationId, Part filePart) {
|
||||
return repository
|
||||
.findById(organizationId, MANAGE_ORGANIZATIONS)
|
||||
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, organizationId)))
|
||||
.flatMap(organization -> {
|
||||
if (filePart != null && filePart.headers().getContentType() != null) {
|
||||
// Default implementation for the BufferFactory used breaks down the FilePart into chunks of 4KB
|
||||
// To limit file size to 250KB, we only allow 63 (250/4 = 62.5) such chunks to be derived from the incoming FilePart
|
||||
return filePart.content().count().flatMap(count -> {
|
||||
if (count > (int) Math.ceil(Constraint.ORGANIZATION_LOGO_SIZE_KB / 4.0)) {
|
||||
return Mono.error(new AppsmithException(AppsmithError.PAYLOAD_TOO_LARGE, Constraint.ORGANIZATION_LOGO_SIZE_KB));
|
||||
} else {
|
||||
return Mono.zip(Mono.just(organization), DataBufferUtils.join(filePart.content()));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Mono.error(new AppsmithException(AppsmithError.VALIDATION_FAILURE, "Please upload a valid image."));
|
||||
}
|
||||
})
|
||||
final Mono<Organization> findOrganizationMono = repository.findById(organizationId, MANAGE_ORGANIZATIONS)
|
||||
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, organizationId)));
|
||||
|
||||
// We don't execute the upload Mono if we don't find the organization.
|
||||
final Mono<Asset> uploadAssetMono = assetService.upload(filePart, Constraint.ORGANIZATION_LOGO_SIZE_KB);
|
||||
|
||||
return findOrganizationMono
|
||||
.flatMap(organization -> Mono.zip(Mono.just(organization), uploadAssetMono))
|
||||
.flatMap(tuple -> {
|
||||
final Organization organization = tuple.getT1();
|
||||
final DataBuffer dataBuffer = tuple.getT2();
|
||||
final Asset uploadedAsset = tuple.getT2();
|
||||
final String prevAssetId = organization.getLogoAssetId();
|
||||
|
||||
byte[] data = new byte[dataBuffer.readableByteCount()];
|
||||
dataBuffer.read(data);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
|
||||
return assetRepository
|
||||
.save(new Asset(filePart.headers().getContentType(), data))
|
||||
.flatMap(asset -> {
|
||||
organization.setLogoAssetId(asset.getId());
|
||||
Mono<Organization> savedOrganization = repository.save(organization);
|
||||
Mono<Asset> createdAsset = analyticsService.sendCreateEvent(asset);
|
||||
return savedOrganization.zipWith(createdAsset);
|
||||
})
|
||||
.flatMap(savedTuple -> {
|
||||
Organization savedOrganization = savedTuple.getT1();
|
||||
if (prevAssetId != null) {
|
||||
return assetRepository.findById(prevAssetId)
|
||||
.flatMap(asset -> assetRepository.delete(asset).thenReturn(asset))
|
||||
.flatMap(analyticsService::sendDeleteEvent)
|
||||
.thenReturn(savedOrganization);
|
||||
} else {
|
||||
organization.setLogoAssetId(uploadedAsset.getId());
|
||||
return repository.save(organization)
|
||||
.flatMap(savedOrganization -> {
|
||||
if (StringUtils.isEmpty(prevAssetId)) {
|
||||
return Mono.just(savedOrganization);
|
||||
} else {
|
||||
return assetService.remove(prevAssetId).thenReturn(savedOrganization);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.appsmith.server.services;
|
|||
|
||||
import com.appsmith.server.domains.User;
|
||||
import com.appsmith.server.domains.UserData;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface UserDataService {
|
||||
|
|
@ -9,9 +11,24 @@ public interface UserDataService {
|
|||
|
||||
Mono<UserData> getForUser(String userId);
|
||||
|
||||
Mono<UserData> getForCurrentUser();
|
||||
|
||||
Mono<UserData> getForUserEmail(String email);
|
||||
|
||||
Mono<UserData> updateForCurrentUser(UserData updates);
|
||||
|
||||
Mono<User> setViewedCurrentVersionReleaseNotes(User user);
|
||||
|
||||
Mono<User> setViewedCurrentVersionReleaseNotes(User user, String version);
|
||||
|
||||
Mono<User> ensureViewedCurrentVersionReleaseNotes(User user);
|
||||
|
||||
Mono<UserData> saveProfilePhoto(Part filePart);
|
||||
|
||||
Mono<Void> deleteProfilePhoto();
|
||||
|
||||
Mono<Void> makeProfilePhotoResponse(ServerWebExchange exchange, String email);
|
||||
|
||||
Mono<Void> makeProfilePhotoResponse(ServerWebExchange exchange);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,46 @@
|
|||
package com.appsmith.server.services;
|
||||
|
||||
import com.appsmith.server.domains.Asset;
|
||||
import com.appsmith.server.domains.QUserData;
|
||||
import com.appsmith.server.domains.User;
|
||||
import com.appsmith.server.domains.UserData;
|
||||
import com.appsmith.server.exceptions.AppsmithError;
|
||||
import com.appsmith.server.exceptions.AppsmithException;
|
||||
import com.appsmith.server.repositories.UserDataRepository;
|
||||
import com.appsmith.server.solutions.ReleaseNotesService;
|
||||
import com.mongodb.DBObject;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
|
||||
import org.springframework.data.mongodb.core.convert.MongoConverter;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.core.query.Update;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
import javax.validation.Validator;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.appsmith.server.repositories.BaseAppsmithRepositoryImpl.fieldName;
|
||||
|
||||
@Service
|
||||
public class UserDataServiceImpl extends BaseService<UserDataRepository, UserData, String> implements UserDataService {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final SessionUserService sessionUserService;
|
||||
|
||||
private final AssetService assetService;
|
||||
|
||||
private final ReleaseNotesService releaseNotesService;
|
||||
|
||||
private static final int MAX_PROFILE_PHOTO_SIZE_KB = 250;
|
||||
|
||||
@Autowired
|
||||
public UserDataServiceImpl(Scheduler scheduler,
|
||||
Validator validator,
|
||||
|
|
@ -29,11 +49,15 @@ public class UserDataServiceImpl extends BaseService<UserDataRepository, UserDat
|
|||
UserDataRepository repository,
|
||||
AnalyticsService analyticsService,
|
||||
UserService userService,
|
||||
SessionUserService sessionUserService,
|
||||
AssetService assetService,
|
||||
ReleaseNotesService releaseNotesService
|
||||
) {
|
||||
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
|
||||
this.userService = userService;
|
||||
this.releaseNotesService = releaseNotesService;
|
||||
this.assetService = assetService;
|
||||
this.sessionUserService = sessionUserService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -50,6 +74,57 @@ public class UserDataServiceImpl extends BaseService<UserDataRepository, UserDat
|
|||
: repository.findByUserId(userId).defaultIfEmpty(new UserData(userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<UserData> getForCurrentUser() {
|
||||
return sessionUserService.getCurrentUser()
|
||||
.map(User::getEmail)
|
||||
.flatMap(this::getForUserEmail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<UserData> getForUserEmail(String email) {
|
||||
return userService.findByEmail(email)
|
||||
.flatMap(this::getForUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<UserData> updateForCurrentUser(UserData updates) {
|
||||
return sessionUserService.getCurrentUser()
|
||||
.flatMap(user -> userService.findByEmail(user.getEmail()))
|
||||
.flatMap(user -> {
|
||||
// If a UserData document exists for this user, update it. If not, create one.
|
||||
updates.setUserId(user.getId());
|
||||
final Mono<UserData> updaterMono = update(user.getId(), updates);
|
||||
final Mono<UserData> creatorMono = Mono.just(updates).flatMap(this::create);
|
||||
return updaterMono.switchIfEmpty(creatorMono);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<UserData> update(String userId, UserData resource) {
|
||||
if (userId == null) {
|
||||
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, fieldName(QUserData.userData.userId)));
|
||||
}
|
||||
|
||||
Query query = new Query(Criteria.where(fieldName(QUserData.userData.userId)).is(userId));
|
||||
|
||||
// In case the update is not used to update the policies, then set the policies to null to ensure that the
|
||||
// existing policies are not overwritten.
|
||||
if (resource.getPolicies().isEmpty()) {
|
||||
resource.setPolicies(null);
|
||||
}
|
||||
|
||||
DBObject update = getDbObject(resource);
|
||||
|
||||
Update updateObj = new Update();
|
||||
Map<String, Object> updateMap = update.toMap();
|
||||
updateMap.entrySet().stream().forEach(entry -> updateObj.set(entry.getKey(), entry.getValue()));
|
||||
|
||||
return mongoTemplate.updateFirst(query, updateObj, resource.getClass())
|
||||
.flatMap(updateResult -> updateResult.getMatchedCount() == 0 ? Mono.empty() : repository.findByUserId(userId))
|
||||
.flatMap(analyticsService::sendUpdateEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<User> setViewedCurrentVersionReleaseNotes(User user) {
|
||||
final String version = releaseNotesService.getReleasedVersion();
|
||||
|
|
@ -86,4 +161,50 @@ public class UserDataServiceImpl extends BaseService<UserDataRepository, UserDat
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<UserData> saveProfilePhoto(Part filePart) {
|
||||
final Mono<String> prevAssetIdMono = getForCurrentUser()
|
||||
.map(userData -> ObjectUtils.defaultIfNull(userData.getProfilePhotoAssetId(), ""));
|
||||
|
||||
final Mono<Asset> uploaderMono = assetService.upload(filePart, MAX_PROFILE_PHOTO_SIZE_KB);
|
||||
|
||||
return Mono.zip(prevAssetIdMono, uploaderMono)
|
||||
.flatMap(tuple -> {
|
||||
final String oldAssetId = tuple.getT1();
|
||||
final Asset uploadedAsset = tuple.getT2();
|
||||
final UserData updates = new UserData();
|
||||
updates.setProfilePhotoAssetId(uploadedAsset.getId());
|
||||
final Mono<UserData> updateMono = updateForCurrentUser(updates);
|
||||
if (StringUtils.isEmpty(oldAssetId)) {
|
||||
return updateMono;
|
||||
} else {
|
||||
return assetService.remove(oldAssetId).then(updateMono);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteProfilePhoto() {
|
||||
return getForCurrentUser()
|
||||
.flatMap(userData -> Mono.justOrEmpty(userData.getProfilePhotoAssetId()))
|
||||
.flatMap(assetService::remove);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> makeProfilePhotoResponse(ServerWebExchange exchange, String email) {
|
||||
return getForUserEmail(email)
|
||||
.flatMap(userData -> makeProfilePhotoResponse(exchange, userData));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> makeProfilePhotoResponse(ServerWebExchange exchange) {
|
||||
return getForCurrentUser()
|
||||
.flatMap(userData -> makeProfilePhotoResponse(exchange, userData));
|
||||
}
|
||||
|
||||
private Mono<Void> makeProfilePhotoResponse(ServerWebExchange exchange, UserData userData) {
|
||||
return Mono.justOrEmpty(userData.getProfilePhotoAssetId())
|
||||
.flatMap(assetId -> assetService.makeImageResponse(exchange, assetId));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,31 @@
|
|||
package com.appsmith.server.services;
|
||||
|
||||
import com.appsmith.server.domains.Asset;
|
||||
import com.appsmith.server.domains.User;
|
||||
import com.appsmith.server.domains.UserData;
|
||||
import com.appsmith.server.exceptions.AppsmithException;
|
||||
import com.appsmith.server.repositories.AssetRepository;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.security.test.context.support.WithUserDetails;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
|
@ -25,6 +40,9 @@ public class UserDataServiceTest {
|
|||
@Autowired
|
||||
private UserDataService userDataService;
|
||||
|
||||
@Autowired
|
||||
private AssetRepository assetRepository;
|
||||
|
||||
private Mono<User> userMono;
|
||||
|
||||
@Before
|
||||
|
|
@ -72,4 +90,81 @@ public class UserDataServiceTest {
|
|||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void testUploadAndDeleteProfilePhoto_validImage() {
|
||||
FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS);
|
||||
Flux<DataBuffer> dataBufferFlux = DataBufferUtils
|
||||
.read(new ClassPathResource("test_assets/OrganizationServiceTest/my_organization_logo.png"), new DefaultDataBufferFactory(), 4096)
|
||||
.cache();
|
||||
|
||||
Mockito.when(filepart.content()).thenReturn(dataBufferFlux);
|
||||
Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.IMAGE_PNG);
|
||||
|
||||
Mono<Tuple2<UserData, Asset>> loadProfileImageMono = userDataService.getForUserEmail("api_user")
|
||||
.flatMap(userData -> Mono.zip(
|
||||
Mono.just(userData),
|
||||
assetRepository.findById(userData.getProfilePhotoAssetId())
|
||||
));
|
||||
|
||||
final Mono<UserData> saveMono = userDataService.saveProfilePhoto(filepart).cache();
|
||||
final Mono<Tuple2<UserData, Asset>> saveAndGetMono = saveMono.then(loadProfileImageMono);
|
||||
final Mono<Tuple2<UserData, Asset>> deleteAndGetMono = saveMono.then(userDataService.deleteProfilePhoto()).then(loadProfileImageMono);
|
||||
|
||||
StepVerifier.create(saveAndGetMono)
|
||||
.assertNext(tuple -> {
|
||||
final UserData userData = tuple.getT1();
|
||||
assertThat(userData.getProfilePhotoAssetId()).isNotNull();
|
||||
|
||||
final Asset asset = tuple.getT2();
|
||||
assertThat(asset).isNotNull();
|
||||
DataBuffer buffer = DataBufferUtils.join(dataBufferFlux).block(Duration.ofSeconds(3));
|
||||
byte[] res = new byte[buffer.readableByteCount()];
|
||||
buffer.read(res);
|
||||
assertThat(asset.getData()).isEqualTo(res);
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(deleteAndGetMono)
|
||||
// Should be empty since the profile photo has been deleted.
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void testUploadProfilePhoto_invalidImageFormat() {
|
||||
FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS);
|
||||
Flux<DataBuffer> dataBufferFlux = DataBufferUtils
|
||||
.read(new ClassPathResource("test_assets/OrganizationServiceTest/my_organization_logo.png"), new DefaultDataBufferFactory(), 4096)
|
||||
.cache();
|
||||
|
||||
Mockito.when(filepart.content()).thenReturn(dataBufferFlux);
|
||||
Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.IMAGE_GIF);
|
||||
|
||||
final Mono<UserData> saveMono = userDataService.saveProfilePhoto(filepart).cache();
|
||||
|
||||
StepVerifier.create(saveMono)
|
||||
.expectErrorMatches(error -> error instanceof AppsmithException)
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void testUploadProfilePhoto_invalidImageSize() {
|
||||
FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS);
|
||||
Flux<DataBuffer> dataBufferFlux = DataBufferUtils
|
||||
.read(new ClassPathResource("test_assets/OrganizationServiceTest/my_organization_logo.png"), new DefaultDataBufferFactory(), 4096)
|
||||
.repeat(70) // So the file size looks like it's much larger than what it actually is.
|
||||
.cache();
|
||||
|
||||
Mockito.when(filepart.content()).thenReturn(dataBufferFlux);
|
||||
Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.IMAGE_PNG);
|
||||
|
||||
final Mono<UserData> saveMono = userDataService.saveProfilePhoto(filepart).cache();
|
||||
|
||||
StepVerifier.create(saveMono)
|
||||
.expectErrorMatches(error -> error instanceof AppsmithException)
|
||||
.verify();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 202 227" version="1.1">
|
||||
<g transform="translate(-319.89 374.57)">
|
||||
<path
|
||||
style="stroke:#2fc500;stroke-width:8.8186;fill:#c8ffbf"
|
||||
d="m422.42-327.99c18.96 0.34 40.63-6.23 41.6-38.09-8.76-0.87-34.18 3.84-35.98 27.68-1.29 17.48-7.93 8.8-10.65-4.07-4.14 1.28-1.12 0.22-6.6 2.32 13.56 21.96 7.09 33.36 9.7 33.16 2.91-0.23 2.01-10.38 1.93-21z"
|
||||
/>
|
||||
<path
|
||||
style="stroke-linejoin:round;stroke:#ec1a00;stroke-width:15;fill:#ffbcb3"
|
||||
d="m351.65-289.46c-32.21 29.23-17.72 102.7 40.19 121.69 26.38 8.51 59.38 6.46 83.59-11.81 19.87-14.72 36.38-36.23 33.41-79.81-1.69-24.5-38.31-68.49-86.37-41.53-13.24-6.75-38.55-18.28-70.82 11.46z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 791 B |
Loading…
Reference in New Issue
Block a user