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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
| package ch.gobothegeek.lofidrox.controllers;
import ch.gobothegeek.lofidrox.model.entities.FileRecipient;
import ch.gobothegeek.lofidrox.model.json.file.*;
import ch.gobothegeek.lofidrox.security.LfdUserBean;
import ch.gobothegeek.lofidrox.security.urls.LfdSecuredUrl;
import ch.gobothegeek.lofidrox.services.FileDescriptorService;
import ch.gobothegeek.lofidrox.services.FileRecipientService;
import ch.gobothegeek.lofidrox.utils.GtgDateUtils;
import org.apache.deltaspike.security.api.authorization.Secured;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
// Manage access point in file package
@RequestScoped
@Path("/file")
@Named
public class FileController {
private final Logger logger = LoggerFactory.getLogger(FileController.class);
@Inject private LfdUserBean lfdUserBean;
@Inject private FileDescriptorService fileDescriptorService;
@Inject private FileRecipientService fileRecipientService;
// url to upload file for users
@POST
@Path("/send")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Secured(LfdSecuredUrl.class)
public Response uploadFile(JsonFileUpload jsonFile) {
JsonFileUploaded reply = new JsonFileUploaded();
if ((null != jsonFile.getFilename()) && (null != jsonFile.getUsers()) && (0 != jsonFile.getUsers().length) && (null != jsonFile.getData())) {
reply.setFilename(jsonFile.getFilename());
reply.setWritten(this.fileDescriptorService.uploadFileToUsers(jsonFile.getUsers(), jsonFile.getFilename(), jsonFile.getData(),
this.lfdUserBean.getLfdUser().getUser()));
}
// return response
return Response.status((reply.getWritten()? Response.Status.OK: Response.Status.BAD_REQUEST)).entity(reply).build();
}
// list files for user
@POST
@Path("/list")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Secured(LfdSecuredUrl.class)
public Response listFiles() {
JsonFilesList reply;
List<FileRecipient> files;
reply = new JsonFilesList();
files = this.fileRecipientService.listFilesForUser(this.lfdUserBean.getLfdUser().getUser());
if ((null != files) && (0 != files.size())) {
for (FileRecipient file : files) {
if (null != file.getFile()) {
reply.getFiles().add(new JsonFileInfo(file.getFileId(), file.getFile().getName(), !file.getDownloaded(), file.getFile().getSource(),
GtgDateUtils.FORMAT_DATE_DDMMYYYYHHMMSS.format(file.getFile().getSendOn())));
}
}
}
return Response.status(Response.Status.OK).entity(reply).build();
}
// return the required file as Base64 stream
@POST
@Path("/download")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Secured(LfdSecuredUrl.class)
public Response downloadFile(JsonFileInfo jsonFileInfo) {
JsonFileDownload jsonDL;
jsonDL = this.fileDescriptorService.downloadFile(jsonFileInfo.getId(), this.lfdUserBean.getLfdUser().getUser());
if (null != jsonDL) {
return Response.status(Response.Status.OK).entity(jsonDL).build();
} else {
logger.error("Unable to fetch file with id [" + jsonFileInfo.getId() + "] for user [" + this.lfdUserBean.getLfdUser().getUser() + "]");
}
return Response.status(Response.Status.BAD_REQUEST).entity(null).build();
}
// delete the specified file. The file must be owned by user calling this url
@DELETE
@Path("/delete")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Secured(LfdSecuredUrl.class)
public Response deleteFiles(JsonFilesDelete jsonDel) {
JsonFilesDeleted jsonRepDel;
int deleted;
jsonRepDel = new JsonFilesDeleted();
if ((null != jsonDel.getFiles()) && (0 != jsonDel.getFiles().size())) {
deleted = this.fileDescriptorService.deleteFiles(jsonDel.getFiles(), this.lfdUserBean.getLfdUser().getUser());
jsonRepDel.setDeletedCount(deleted);
}
return Response.status((-1 < jsonRepDel.getDeletedCount()? Response.Status.OK : Response.Status.BAD_REQUEST)).entity(jsonRepDel).build();
}
}
|
Contrôleur traitant les utilisateurs#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
| package ch.gobothegeek.lofidrox.controllers;
import ch.gobothegeek.lofidrox.exceptions.LfdException;
import ch.gobothegeek.lofidrox.model.entities.User;
import ch.gobothegeek.lofidrox.model.json.user.*;
import ch.gobothegeek.lofidrox.security.LfdSecured;
import ch.gobothegeek.lofidrox.security.urls.LfdSecuredUrl;
import ch.gobothegeek.lofidrox.security.urls.LfdUnsecuredUrl;
import ch.gobothegeek.lofidrox.services.SessionService;
import ch.gobothegeek.lofidrox.services.UserService;
import org.apache.deltaspike.security.api.authorization.AccessDeniedException;
import org.apache.deltaspike.security.api.authorization.Secured;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
// Manage access point in user package
@RequestScoped
@Path("/user")
@Named
public class UserController {
private final Logger logger = LoggerFactory.getLogger(UserController.class);
@Inject private UserService userService;
@Inject private SessionService sessionService;
// url to log in user
@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Secured(LfdUnsecuredUrl.class)
public Response loginUser(JsonUserLogin nameLog) {
Boolean isOk;
// requires user login
isOk = this.userService.loginUser(nameLog.getUsername(), nameLog.getPwd());
// clear password
nameLog.setPwd(null);
// add status
nameLog.setLogged(isOk);
// return response
return Response.status((isOk? Response.Status.OK: Response.Status.UNAUTHORIZED)).entity(nameLog).build();
}
// url to log out user
@DELETE
@Path("/logout")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Secured(LfdSecuredUrl.class)
@LfdSecured
public Response logoutUser(JsonUserLogout nameLog) {
// ask for log out
this.userService.logoffUser(nameLog.getUsername());
// add status
nameLog.setExited(true);
// return response
return Response.status(Response.Status.OK).entity(nameLog).build();
}
// url to register user account
@POST
@Path("/register")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response registerUser(JsonUserRegister nameReg) {
User reg;
try {
// require user creation
reg = this.userService.createUser(nameReg.getUsername(), nameReg.getPwd());
if (null != reg) {
// user is registered
nameReg.setRegistered(true);
// clear password
nameReg.setPwd(null);
// return OK response
return Response.status(Response.Status.OK).entity(nameReg).build();
}
// user is not registerd because it already exists
return Response.status(Response.Status.NOT_ACCEPTABLE).entity(null).build();
} catch (LfdException e) {
// user is not registerd due to technical error
return Response.status(Response.Status.BAD_REQUEST).entity(null).build();
}
}
// url used to check if user has a valid session
@POST
@Path("/check")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response checkUserSession(JsonUserCheck userCheck) {
userCheck.setSession(false);
try {
// add status
userCheck.setSession(this.securedCheckUserSession(userCheck.getUsername()));
// return response
} catch (AccessDeniedException e) {
logger.error("User [" + userCheck.getUsername() + "] is not logged in.", e);
this.sessionService.delete(userCheck.getUsername()); // remove session
}
return Response.status((userCheck.getSession()? Response.Status.OK : Response.Status.UNAUTHORIZED)).entity(userCheck).build();
}
// we have to handle a call through secured method to check if user is really logged or not
@Secured(LfdSecuredUrl.class)
@LfdSecured
private boolean securedCheckUserSession(String username) {
return this.sessionService.hasSession(username);
}
// url used to return user's list
@POST
@Path("/list")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Secured(LfdSecuredUrl.class)
@LfdSecured
public Response getUsersList() {
JsonUsersList json = new JsonUsersList();
json.setUsers(this.userService.listUsernames());
// return response
return Response.status(Response.Status.OK).entity(json).build();
}
}
|
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
C’est par ici