Dans ce deuxième article, nous allons nous intéresser aux contrôleurs REST de l’application.

Structure REST

Dans LoFiDroX, il y a deux catégories de données: les fichiers et les utilisateurs. Il est donc nécessaire de sectoriser les urls afin de faciliter la compréhension et éviter des problèmes de nommage (endpoint en double par exemple).

Contrôleur traitant les fichiers

Les points à noter sont:

  • tous les endpoints sont protégés (annotation @Secured): l’utilisateur doit être identifié pour les utiliser
  • tous les endpoints reçoivent et retourne du JSON (annotations @Consumes et @Produces).
  • les fichiers sont transmis en Base64. Pour des fichiers de taille raisonnable, ça fait le job. Bien sûr, envoyer une archive contenant l’intégrale de Star Wars en 8K permettra de dire adieu au Raspberry Pi.
    
  1package ch.gobothegeek.lofidrox.controllers;
  2
  3import ch.gobothegeek.lofidrox.model.entities.FileRecipient;
  4import ch.gobothegeek.lofidrox.model.json.file.*;
  5import ch.gobothegeek.lofidrox.security.LfdUserBean;
  6import ch.gobothegeek.lofidrox.security.urls.LfdSecuredUrl;
  7import ch.gobothegeek.lofidrox.services.FileDescriptorService;
  8import ch.gobothegeek.lofidrox.services.FileRecipientService;
  9import ch.gobothegeek.lofidrox.utils.GtgDateUtils;
 10import org.apache.deltaspike.security.api.authorization.Secured;
 11import org.slf4j.Logger;
 12import org.slf4j.LoggerFactory;
 13import javax.enterprise.context.RequestScoped;
 14import javax.inject.Inject;
 15import javax.inject.Named;
 16import javax.ws.rs.*;
 17import javax.ws.rs.core.MediaType;
 18import javax.ws.rs.core.Response;
 19import java.util.List;
 20
 21// Manage access point in file package
 22@RequestScoped
 23@Path("/file")
 24@Named
 25public class FileController {
 26	private final Logger logger = LoggerFactory.getLogger(FileController.class);
 27
 28	@Inject	private LfdUserBean lfdUserBean;
 29	@Inject private FileDescriptorService fileDescriptorService;
 30	@Inject private FileRecipientService fileRecipientService;
 31
 32	// url to upload file for users
 33	@POST
 34	@Path("/send")
 35	@Consumes(MediaType.APPLICATION_JSON)
 36	@Produces(MediaType.APPLICATION_JSON)
 37	@Secured(LfdSecuredUrl.class)
 38	public Response uploadFile(JsonFileUpload jsonFile) {
 39		JsonFileUploaded reply = new JsonFileUploaded();
 40
 41		if ((null != jsonFile.getFilename()) && (null != jsonFile.getUsers()) && (0 != jsonFile.getUsers().length) && (null != jsonFile.getData())) {
 42			reply.setFilename(jsonFile.getFilename());
 43			reply.setWritten(this.fileDescriptorService.uploadFileToUsers(jsonFile.getUsers(), jsonFile.getFilename(), jsonFile.getData(),
 44					this.lfdUserBean.getLfdUser().getUser()));
 45		}
 46		// return response
 47		return Response.status((reply.getWritten()? Response.Status.OK: Response.Status.BAD_REQUEST)).entity(reply).build();
 48	}
 49
 50	// list files for user
 51	@POST
 52	@Path("/list")
 53	@Consumes(MediaType.APPLICATION_JSON)
 54	@Produces(MediaType.APPLICATION_JSON)
 55	@Secured(LfdSecuredUrl.class)
 56	public Response listFiles() {
 57		JsonFilesList reply;
 58		List<FileRecipient> files;
 59
 60		reply = new JsonFilesList();
 61		files = this.fileRecipientService.listFilesForUser(this.lfdUserBean.getLfdUser().getUser());
 62		if ((null != files) && (0 != files.size())) {
 63			for (FileRecipient file : files) {
 64				if (null != file.getFile()) {
 65					reply.getFiles().add(new JsonFileInfo(file.getFileId(), file.getFile().getName(), !file.getDownloaded(), file.getFile().getSource(),
 66							GtgDateUtils.FORMAT_DATE_DDMMYYYYHHMMSS.format(file.getFile().getSendOn())));
 67				}
 68			}
 69		}
 70		return Response.status(Response.Status.OK).entity(reply).build();
 71	}
 72
 73	// return the required file as Base64 stream
 74	@POST
 75	@Path("/download")
 76	@Consumes(MediaType.APPLICATION_JSON)
 77	@Produces(MediaType.APPLICATION_JSON)
 78	@Secured(LfdSecuredUrl.class)
 79	public Response downloadFile(JsonFileInfo jsonFileInfo) {
 80		JsonFileDownload jsonDL;
 81
 82		jsonDL = this.fileDescriptorService.downloadFile(jsonFileInfo.getId(), this.lfdUserBean.getLfdUser().getUser());
 83		if (null != jsonDL) {
 84				return Response.status(Response.Status.OK).entity(jsonDL).build();
 85		} else {
 86			logger.error("Unable to fetch file with id [" + jsonFileInfo.getId() + "] for user [" + this.lfdUserBean.getLfdUser().getUser() + "]");
 87		}
 88		return Response.status(Response.Status.BAD_REQUEST).entity(null).build();
 89	}
 90
 91	// delete the specified file. The file must be owned by user calling this url
 92	@DELETE
 93	@Path("/delete")
 94	@Consumes(MediaType.APPLICATION_JSON)
 95	@Produces(MediaType.APPLICATION_JSON)
 96	@Secured(LfdSecuredUrl.class)
 97	public Response deleteFiles(JsonFilesDelete jsonDel) {
 98		JsonFilesDeleted jsonRepDel;
 99		int deleted;
100
101		jsonRepDel = new JsonFilesDeleted();
102		if ((null != jsonDel.getFiles()) && (0 != jsonDel.getFiles().size())) {
103			deleted = this.fileDescriptorService.deleteFiles(jsonDel.getFiles(), this.lfdUserBean.getLfdUser().getUser());
104			jsonRepDel.setDeletedCount(deleted);
105		}
106		return Response.status((-1 < jsonRepDel.getDeletedCount()? Response.Status.OK : Response.Status.BAD_REQUEST)).entity(jsonRepDel).build();
107	}
108}

Contrôleur traitant les utilisateurs

    
  1package ch.gobothegeek.lofidrox.controllers;
  2
  3import ch.gobothegeek.lofidrox.exceptions.LfdException;
  4import ch.gobothegeek.lofidrox.model.entities.User;
  5import ch.gobothegeek.lofidrox.model.json.user.*;
  6import ch.gobothegeek.lofidrox.security.LfdSecured;
  7import ch.gobothegeek.lofidrox.security.urls.LfdSecuredUrl;
  8import ch.gobothegeek.lofidrox.security.urls.LfdUnsecuredUrl;
  9import ch.gobothegeek.lofidrox.services.SessionService;
 10import ch.gobothegeek.lofidrox.services.UserService;
 11import org.apache.deltaspike.security.api.authorization.AccessDeniedException;
 12import org.apache.deltaspike.security.api.authorization.Secured;
 13import org.slf4j.Logger;
 14import org.slf4j.LoggerFactory;
 15import javax.enterprise.context.RequestScoped;
 16import javax.inject.Inject;
 17import javax.inject.Named;
 18import javax.ws.rs.*;
 19import javax.ws.rs.core.MediaType;
 20import javax.ws.rs.core.Response;
 21
 22// Manage access point in user package
 23@RequestScoped
 24@Path("/user")
 25@Named
 26public class UserController {
 27	private final Logger logger = LoggerFactory.getLogger(UserController.class);
 28
 29	@Inject private UserService userService;
 30	@Inject private SessionService sessionService;
 31
 32	// url to log in user
 33	@POST
 34	@Path("/login")
 35	@Consumes(MediaType.APPLICATION_JSON)
 36	@Produces(MediaType.APPLICATION_JSON)
 37	@Secured(LfdUnsecuredUrl.class)
 38	public Response loginUser(JsonUserLogin nameLog) {
 39		Boolean isOk;
 40
 41		// requires user login
 42		isOk = this.userService.loginUser(nameLog.getUsername(), nameLog.getPwd());
 43		// clear password
 44		nameLog.setPwd(null);
 45		// add status
 46		nameLog.setLogged(isOk);
 47		// return response
 48		return Response.status((isOk? Response.Status.OK: Response.Status.UNAUTHORIZED)).entity(nameLog).build();
 49	}
 50
 51	// url to log out user
 52	@DELETE
 53	@Path("/logout")
 54	@Consumes(MediaType.APPLICATION_JSON)
 55	@Produces(MediaType.APPLICATION_JSON)
 56	@Secured(LfdSecuredUrl.class)
 57	@LfdSecured
 58	public Response logoutUser(JsonUserLogout nameLog) {
 59		// ask for log out
 60		this.userService.logoffUser(nameLog.getUsername());
 61		// add status
 62		nameLog.setExited(true);
 63		// return response
 64		return Response.status(Response.Status.OK).entity(nameLog).build();
 65	}
 66
 67	// url to register user account
 68	@POST
 69	@Path("/register")
 70	@Consumes(MediaType.APPLICATION_JSON)
 71	@Produces(MediaType.APPLICATION_JSON)
 72	public Response registerUser(JsonUserRegister nameReg) {
 73		User reg;
 74
 75		try {
 76			// require user creation
 77			reg = this.userService.createUser(nameReg.getUsername(), nameReg.getPwd());
 78			if (null != reg) {
 79				// user is registered
 80				nameReg.setRegistered(true);
 81				// clear password
 82				nameReg.setPwd(null);
 83				// return OK response
 84				return Response.status(Response.Status.OK).entity(nameReg).build();
 85			}
 86			// user is not registerd because it already exists
 87			return Response.status(Response.Status.NOT_ACCEPTABLE).entity(null).build();
 88		} catch (LfdException e) {
 89			// user is not registerd due to technical error
 90			return Response.status(Response.Status.BAD_REQUEST).entity(null).build();
 91		}
 92	}
 93
 94	// url used to check if user has a valid session
 95	@POST
 96	@Path("/check")
 97	@Consumes(MediaType.APPLICATION_JSON)
 98	@Produces(MediaType.APPLICATION_JSON)
 99	public Response checkUserSession(JsonUserCheck userCheck) {
100		userCheck.setSession(false);
101		try {
102			// add status
103			userCheck.setSession(this.securedCheckUserSession(userCheck.getUsername()));
104			// return response
105		} catch (AccessDeniedException e) {
106			logger.error("User [" + userCheck.getUsername() + "] is not logged in.", e);
107			this.sessionService.delete(userCheck.getUsername()); // remove session
108		}
109		return Response.status((userCheck.getSession()? Response.Status.OK : Response.Status.UNAUTHORIZED)).entity(userCheck).build();
110	}
111
112	// we have to handle a call through secured method to check if user is really logged or not
113	@Secured(LfdSecuredUrl.class)
114	@LfdSecured
115	private boolean securedCheckUserSession(String username) {
116		return this.sessionService.hasSession(username);
117	}
118
119	// url used to return user's list
120	@POST
121	@Path("/list")
122	@Consumes(MediaType.APPLICATION_JSON)
123	@Produces(MediaType.APPLICATION_JSON)
124	@Secured(LfdSecuredUrl.class)
125	@LfdSecured
126	public Response getUsersList() {
127		JsonUsersList json = new JsonUsersList();
128		json.setUsers(this.userService.listUsernames());
129		// return response
130		return Response.status(Response.Status.OK).entity(json).build();
131	}
132}

Ce contrôleur, tout comme FileController, reçoit et retourne du JSON uniquement. Plusieurs particularités sont à noter:

  • la méthode loginUser est annotée @Secured(LfdUnsecuredUrl.class) afin de pouvoir être utilisée lorsqu’on n’est pas connecté (logique).
  • la méthode checkUserSession permet au client de savoir s’il existe encore une session pour l’utilisateur. Cette méthode n’est pas sécurisée mais utilise une méthode privée qui est sécurisée. L’objectif est de determiner si l’utilisateur est effectivement toujours connecté ou non. Si on avait sécurisé la méthode checkUserSession, Deltaspike aurait levé une exception en cas d’appel non authentifié.

Conclusion

Si on s’affranchit de la problématique de streamer les fichiers volumineux, l’écriture des contrôleurs et leur sécurisation est plutôt aisée avec Apache Deltaspike.

Code source

Comme annoncé dans l’article Fuyez GitHub, le code n’est plus disponible sur GitHub. L’intégralité du code est disponible sur mon CodeBerg: https://codeberg.org/GoboTheGeek/LoFiDroX