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