User3Resource.java
/*
* Copyright © 2022-2025 The CTAN Team and individual authors
*
* This file is distributed under the 3-clause BSD license.
* See file LICENSE for details.
*/
package org.ctan.site.resources.site;
import java.util.Map;
import org.ctan.site.domain.Gender;
import org.ctan.site.domain.account.Ticket;
import org.ctan.site.services.account.AccountService;
import org.ctan.site.services.account.AccountService.To;
import org.ctan.site.services.account.AccountService.UserTo;
import org.ctan.site.services.account.JwtManager;
import org.ctan.site.stores.UserStore;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.dropwizard.hibernate.UnitOfWork;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response.Status;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
/**
* The class <code>User3Resource</code> contains the controller for the user
* resource. The end-points contained herein start with <code>/3.0/user/</code>.
*
* @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
*/
@Path("/3.0/user")
@Produces(MediaType.APPLICATION_JSON)
public class User3Resource {
/**
* The class <code>CtanSiteConfigTo</code> contains the transport object for
* the configuration resource.
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
protected static class CtanSiteConfigTo {
private String name;
private String version;
private String[] languages;
}
/**
* The class <code>LoginRequest</code> contains the transport object for the
* login request.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class LoginRequest {
private String account;
private String password;
}
/**
* The class <code>PasswdRequest</code> contains the transport object for
* the password request.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PasswdRequest {
private String ticket;
private String password;
}
/**
* The class <code>PasswdTokenRequest</code> contains the transport object
* for the password token request.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PasswdTokenRequest {
private String account;
private String email;
private String locale;
}
/**
* The class <code>PasswordRequest</code> contains the transport object for
* the password request.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PasswordRequest {
private String token;
private String password;
}
/**
* The class <code>RefreshRequest</code> contains the transport object for
* the refresh request.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class RefreshRequest {
private String account;
private String refresh;
}
/**
* The class <code>RegisterRequest</code> contains the transport object for
* the register request.
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class RegisterRequest {
private String account;
private String email;
private Gender gender;
private String lang;
private String name;
}
/**
* The class <code>RemoveReq</code> contains the parameters for the remove
* request.
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
protected static class RemoveRequest {
private String account;
private String token;
}
/**
* The class <code>SetRequest</code> contains the request object for the set
* request.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SetRequest {
private String token;
private String attribute;
private String value;
}
/**
* The class <code>UserInfo</code> contains the transport object for the
* user info request.
*/
@Data
@Builder
public static class UserInfo {
private String account;
private String name;
private String author;
private String email;
private String description;
private String gender;
}
/**
* The field <code>service</code> contains the underlying service.
*/
private AccountService service;
/**
* This is the constructor for the class <code>User3Resource</code>.
*
* @param service the underlying service
*/
@SuppressFBWarnings(value = {"CT_CONSTRUCTOR_THROW", "EI_EXPOSE_REP2"})
public User3Resource(@NonNull AccountService service) {
this.service = service;
}
/**
* The method <code>exists</code> provides means to check whether a user
* exists.
*
* @param account the account or {@code null}
* @param email the email address or {@code null}
* @return {@code true} iff the user exists
*/
@GET
@Path("/exists")
@PermitAll
@UnitOfWork(value = "siteDb")
public Boolean exists(@QueryParam("account") String account,
@QueryParam("email") String email) {
if (!AccountService.isEmpty(account)) {
var user = service.getStore().getByAccount(account);
if (user == null) {
return false;
}
return AccountService.isEmpty(email)
|| email.equalsIgnoreCase(user.getEmail());
} else if (!AccountService.isEmpty(email)) {
return service.getStore().getByEmail(email) != null;
}
return false;
}
/**
* The method <code>info</code> provides means to retrieve public data of a
* user.
*
* @param account the account name
* @return he user info transport object
*/
@GET
@Path("/info")
@PermitAll
@UnitOfWork(value = "siteDb")
public UserInfo info(@QueryParam("account") String account) {
if (AccountService.isEmpty(account)) {
throw new WebApplicationException(Status.NOT_FOUND);
}
var user = service.getStore().getByAccount(account);
if (user == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
String desc = user.getSelfDescription();
var builder = UserInfo.builder()
.account(user.getAccount())
.description(desc != null ? desc : "")
.gender(user.getGender().getValue());
if (user.getShowName()) {
builder.name(user.getName());
}
if (user.getShowEmail()) {
builder.email(user.getEmail());
}
var authorKey = user.getAuthorKey();
if (!AccountService.isEmpty(authorKey)) {
builder.author(authorKey);
}
return builder.build();
}
/**
* The method <code>login</code> provides means to perform a login.
*
* @param req the request containing account and password
* @return the user info or {@code null}
*/
@POST
@Path("/login")
@PermitAll
@UnitOfWork(value = "siteDb")
public UserTo login(LoginRequest req) {
var login = service.login(req.getAccount(), req.getPassword());
if (login == null) {
throw new WebApplicationException("unknown user", Status.NOT_FOUND);
}
return login;
}
/**
* The method <code>login</code> provides means to perform a login.
*
* @param req the request containing account and refresh token
* @return the user info or {@code null}
*/
@POST
@Path("/refresh")
@PermitAll
@UnitOfWork(value = "siteDb")
public UserTo refresh(RefreshRequest req) {
if (req == null
|| req.getAccount() == null
|| req.getRefresh() == null) {
throw new WebApplicationException("missing arguments",
Status.BAD_REQUEST);
}
UserTo login = service.refresh(req.getAccount(), req.getRefresh());
if (login == null) {
throw new WebApplicationException("unknown user", Status.NOT_FOUND);
}
return login;
}
/**
* The method <code>registerAccount</code> provides means to request a new
* account.
*
* @param registerRequest the request
* @return the description of the account or the errors encountered
*/
@POST
@Path("/register")
@PermitAll
@UnitOfWork(value = "siteDb")
public To registerAccount(@NonNull RegisterRequest registerRequest) {
return service.registerAccount(registerRequest.getAccount(),
registerRequest.getEmail(), registerRequest.getName(),
registerRequest.getGender(), registerRequest.getLang());
}
/**
* The method <code>remove</code> provides means to delete an account.
*
* @param request the request parameters
*/
@DELETE
@PermitAll
@UnitOfWork(value = "siteDb")
public void remove(RemoveRequest request) {
String account = request.getAccount();
if (AccountService.isEmpty(account)) {
throw new WebApplicationException(Status.FORBIDDEN);
}
UserStore store = service.getStore();
var user = store.getByAccount(account);
if (user == null) {
throw new WebApplicationException(Status.FORBIDDEN);
}
var acc = JwtManager.verifyAuth(request.getToken());
if (acc == null || !acc.equals(account)) {
throw new WebApplicationException(Status.FORBIDDEN);
}
store.remove(user);
}
/**
* The method <code>requestPasswdTicket</code> provides means to set the
* password.
*
* @param req the parameters
* @return the new ticket
*/
@POST
@Path("/passwd")
@PermitAll
@UnitOfWork(value = "siteDb")
public Ticket requestPasswdTicket(PasswdTokenRequest req) {
if (req == null) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
Ticket ticket = service.passwd(req.getAccount(), req.getEmail(),
req.getLocale());
if (ticket == null) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
return ticket;
}
/**
* The method <code>set</code> provides means to set a single attribute and
* store the entity.
*
* @param request the request container object
* @return the success indicator
*/
@POST
@Path("/set")
@PermitAll // TODO
@UnitOfWork(value = "siteDb")
@SuppressFBWarnings(value = "DCN_NULLPOINTER_EXCEPTION")
public boolean set(@NonNull SetRequest request) {
var account = JwtManager.verifyAuth(request.getToken());
if (account == null) {
throw new WebApplicationException(Status.FORBIDDEN);
}
try {
service.set(account, request.getAttribute(), request.getValue());
} catch (IllegalArgumentException | NullPointerException e) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
return true;
}
/**
* The method <code>setPasswd</code> provides means to set the password.
*
* @param req the transport object
* @return the user name in a map
*/
@POST
@Path("/set/passwd")
@PermitAll // TODO
@UnitOfWork(value = "siteDb")
public boolean setPasswd(PasswdRequest req) {
if (req == null || req.getPassword() == null
|| req.getTicket() == null) {
return false;
}
return service.setPasswdFromTicket(req.getTicket(), req.getPassword());
}
/**
* The method <code>setPassword</code> provides means to set the password.
*
* @param req the transport object
*/
@POST
@Path("/set/password")
@PermitAll
@UnitOfWork(value = "siteDb")
public void setPassword(PasswordRequest req) {
if (req == null || req.getPassword() == null
|| req.getToken() == null) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
var account = JwtManager.verifyAuth(req.getToken());
if (account == null) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
service.setPassword(account, req.getPassword());
}
/**
* The method <code>ticket</code> provides means to check that a ticket
* exists and is not expired.
*
* @param ticket the ticket
* @return the user name
*/
@GET
@Path("/ticket/{ticket}")
@PermitAll
@UnitOfWork(value = "siteDb")
public Map<String, String> ticket(@PathParam("ticket") String ticket) {
var t = service.verifyTicket(ticket);
if (t == null) {
return null;
}
return Map.of("account", t.getAccount());
}
}