From 146a0fb8b0e90f9196e569152f649baf60d6cc8f Mon Sep 17 00:00:00 2001
From: Joaquín Reñé <jrene@curisit.net>
Date: Tue, 07 Oct 2025 14:52:57 +0000
Subject: [PATCH] #4410 - Comments on classes

---
 securis/src/main/java/net/curisit/securis/services/UserResource.java |  643 ++++++++++++++++++++++++++++++++++++----------------------
 1 files changed, 396 insertions(+), 247 deletions(-)

diff --git a/securis/src/main/java/net/curisit/securis/services/UserResource.java b/securis/src/main/java/net/curisit/securis/services/UserResource.java
index d5a9690..7c4681a 100644
--- a/securis/src/main/java/net/curisit/securis/services/UserResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/UserResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
 package net.curisit.securis.services;
 
 import java.util.Date;
@@ -47,291 +50,437 @@
 import net.curisit.securis.utils.TokenHelper;
 
 /**
- * User resource
- * 
+ * UserResource
+ * <p>
+ * REST resource that manages users (CRUD + authentication helpers).
+ * All endpoints are guarded and ADMIN-only unless otherwise stated.
+ * <p>
+ * Notes:
+ * - Uses {@link BasicSecurityContext} authorization via @Securable and @RolesAllowed.
+ * - Uses JPA {@link EntityManager} injected through @Context.
+ * - Mutating endpoints are wrapped in @EnsureTransaction to guarantee commit/rollback.
+ * - Passwords are stored as SHA-256 hashes (see {@link Utils#sha256(String)}).
+ *
+ * Endpoints:
+ *  GET  /user/              -> list users
+ *  GET  /user/{uid}         -> get user by username
+ *  POST /user/              -> create user (idempotent: upsert semantics)
+ *  PUT  /user/{uid}         -> update user (creates if not exists)
+ *  POST /user/login         -> password authentication; returns token and basic identity
+ *  POST /user/check         -> validates a token and returns token metadata
+ *  GET  /user/logout        -> invalidates HTTP session (non-token based)
+ *
+ * Thread-safety: RequestScoped. No shared mutable state.
+ *
  * @author roberto <roberto.sanchez@curisit.net>
+ * Last reviewed by JRA on Oct 5, 2025.
  */
 @Path("/user")
 @RequestScoped
 public class UserResource {
 
-	@Inject
-	TokenHelper tokenHelper;
+    /** Token encoder/decoder & validator. */
+    @Inject TokenHelper tokenHelper;
 
-	@Inject
-	private CacheTTL cache;
+    /** Small cache to invalidate role/org derived data after user mutations. */
+    @Inject private CacheTTL cache;
 
-	@Context
-	EntityManager em;
+    /** JPA entity manager bound to the current request context. */
+    @Context EntityManager em;
 
-	private static final Logger LOG = LogManager.getLogger(UserResource.class);
+    private static final Logger LOG = LogManager.getLogger(UserResource.class);
 
-	public UserResource() {
-	}
+    /**
+     * UserResource 
+     * Default constructor for CDI. 
+     */
+    public UserResource() {
+    }
 
-	/**
-	 * 
-	 * @return the server version in format majorVersion.minorVersion
-	 */
-	@GET
-	@Path("/")
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response index() {
-		LOG.info("Getting users list ");
+    // ---------------------------------------------------------------------
+    // Read operations
+    // ---------------------------------------------------------------------
 
-		// EntityManager em = emProvider.get();
-		em.clear();
-		TypedQuery<User> q = em.createNamedQuery("list-users", User.class);
+    /**
+     * index
+     * <p>
+     * List all users.
+     *
+     * Security: ADMIN only.
+     *
+     * @return 200 OK with JSON array of {@link User}, or 200 OK with empty list.
+     */
+    @GET
+    @Path("/")
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response index() {
+        LOG.info("Getting users list ");
 
-		List<User> list = q.getResultList();
+        em.clear();
+        TypedQuery<User> q = em.createNamedQuery("list-users", User.class);
+        List<User> list = q.getResultList();
 
-		return Response.ok(list).build();
-	}
+        return Response.ok(list).build();
+    }
 
-	/**
-	 * 
-	 * @return The user
-	 */
-	@GET
-	@Path("/{uid}")
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response get(@PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		LOG.info("Getting user data for id: {}: ", uid);
-		if (uid == null || "".equals(uid)) {
-			LOG.error("User ID is mandatory");
-			return Response.status(Status.NOT_FOUND).build();
-		}
+    /**
+     * get
+     * <p>
+     * Retrieve a single user by username.
+     *
+     * Security: ADMIN only.
+     *
+     * @param uid Username (primary key).
+     * @param token Optional token header (unused here, enforced by filters).
+     * @return 200 OK with user payload or 404 if not found/invalid uid.
+     */
+    @GET
+    @Path("/{uid}")
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response get(@PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        LOG.info("Getting user data for id: {}: ", uid);
+        if (uid == null || "".equals(uid)) {
+            LOG.error("User ID is mandatory");
+            return Response.status(Status.NOT_FOUND).build();
+        }
 
-		// EntityManager em = emProvider.get();
-		em.clear();
-		User lt = em.find(User.class, uid);
-		if (lt == null) {
-			LOG.error("User with id {} not found in DB", uid);
-			return Response.status(Status.NOT_FOUND).build();
-		}
-		return Response.ok(lt).build();
-	}
+        em.clear();
+        User lt = em.find(User.class, uid);
+        if (lt == null) {
+            LOG.error("User with id {} not found in DB", uid);
+            return Response.status(Status.NOT_FOUND).build();
+        }
+        return Response.ok(lt).build();
+    }
 
-	@POST
-	@Path("/")
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@EnsureTransaction
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response create(User user, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		LOG.info("Creating new user");
-		// EntityManager em = emProvider.get();
-		User currentUser = em.find(User.class, user.getUsername());
-		if (currentUser != null) {
-			LOG.info("User with id {} was found in DB, we'll try to modify it", user.getUsername());
-			return modify(user, user.getUsername(), token);
-		}
+    // ---------------------------------------------------------------------
+    // Create / Update / Delete
+    // ---------------------------------------------------------------------
 
-		try {
-			this.setUserOrgs(user, user.getOrgsIds(), em);
-		} catch (SeCurisException e) {
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
-		}
-		try {
-			this.setUserApps(user, user.getAppsIds(), em);
-		} catch (SeCurisException e) {
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
-		}
-		if (user.getPassword() != null && !"".equals(user.getPassword())) {
-			user.setPassword(Utils.sha256(user.getPassword()));
-		} else {
-			return Response.status(DefaultExceptionHandler.DEFAULT_APP_ERROR_STATUS_CODE).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "User password is mandatory")
-					.build();
-		}
-		user.setModificationTimestamp(new Date());
-		user.setLastLogin(null);
-		user.setCreationTimestamp(new Date());
-		em.persist(user);
+    /**
+     * create
+     * <p>
+     * Create a new user. If the username already exists, delegates to {@link #modify(User, String, String)}
+     * to behave like an upsert.
+     *
+     * Security: ADMIN only.
+     * Transaction: yes (via @EnsureTransaction).
+     *
+     * @param user  Incoming user payload. Password must be non-empty (plain text).
+     *              Password is SHA-256 hashed before persist.
+     * @param token Security token header (unused here; enforced by filters).
+     * @return 200 OK with created/updated user; 4xx on validation errors.
+     */
+    @POST
+    @Path("/")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @EnsureTransaction
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response create(User user, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        LOG.info("Creating new user");
 
-		return Response.ok(user).build();
-	}
+        User currentUser = em.find(User.class, user.getUsername());
+        if (currentUser != null) {
+            LOG.info("User with id {} was found in DB, we'll try to modify it", user.getUsername());
+            return modify(user, user.getUsername(), token);
+        }
 
-	private void setUserOrgs(User user, Set<Integer> orgsIds, EntityManager em) throws SeCurisException {
-		Set<Organization> orgs = null;
-		if (orgsIds != null && !orgsIds.isEmpty()) {
-			orgs = new HashSet<>();
-			for (Integer orgId : orgsIds) {
-				Organization o = em.find(Organization.class, orgId);
-				if (o == null) {
-					LOG.error("User organization with id {} not found in DB", orgId);
-					throw new SeCurisException("User's organization not found with ID: " + orgId);
-				}
-				orgs.add(o);
-			}
-		}
+        try {
+            this.setUserOrgs(user, user.getOrgsIds(), em);
+        } catch (SeCurisException e) {
+            return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+        }
+        try {
+            this.setUserApps(user, user.getAppsIds(), em);
+        } catch (SeCurisException e) {
+            return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+        }
 
-		user.setOrganizations(orgs);
+        // Password must be provided on create
+        if (user.getPassword() != null && !"".equals(user.getPassword())) {
+            user.setPassword(Utils.sha256(user.getPassword()));
+        } else {
+            return Response.status(DefaultExceptionHandler.DEFAULT_APP_ERROR_STATUS_CODE)
+                    .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "User password is mandatory")
+                    .build();
+        }
 
-	}
+        user.setModificationTimestamp(new Date());
+        user.setLastLogin(null);
+        user.setCreationTimestamp(new Date());
+        em.persist(user);
 
-	private void setUserApps(User user, Set<Integer> appsIds, EntityManager em) throws SeCurisException {
-		Set<Application> apps = null;
-		if (appsIds != null && !appsIds.isEmpty()) {
-			apps = new HashSet<>();
-			for (Integer appId : appsIds) {
-				Application o = em.find(Application.class, appId);
-				if (o == null) {
-					LOG.error("User application with id {} not found in DB", appId);
-					throw new SeCurisException("User's application not found with ID: " + appId);
-				}
-				apps.add(o);
-			}
-		}
+        return Response.ok(user).build();
+    }
 
-		user.setApplications(apps);
-	}
+    /**
+     * setUserOrgs
+     * <p>
+     * Resolve and set the organizations for a user from a set of IDs.
+     * Validates each id exists in DB.
+     *
+     * @param user     Target user entity.
+     * @param orgsIds  Organization ids to assign (nullable/empty allowed).
+     * @param em       EntityManager.
+     * @throws SeCurisException if any of the referenced organizations does not exist.
+     */
+    private void setUserOrgs(User user, Set<Integer> orgsIds, EntityManager em) throws SeCurisException {
+        Set<Organization> orgs = null;
+        if (orgsIds != null && !orgsIds.isEmpty()) {
+            orgs = new HashSet<>();
+            for (Integer orgId : orgsIds) {
+                Organization o = em.find(Organization.class, orgId);
+                if (o == null) {
+                    LOG.error("User organization with id {} not found in DB", orgId);
+                    throw new SeCurisException("User's organization not found with ID: " + orgId);
+                }
+                orgs.add(o);
+            }
+        }
+        user.setOrganizations(orgs);
+    }
 
-	@PUT
-	@POST
-	@Path("/{uid}")
-	@EnsureTransaction
-	@Consumes(MediaType.APPLICATION_JSON)
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response modify(User user, @PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
-		LOG.info("Modifying user with id: {}", uid);
-		// EntityManager em = emProvider.get();
-		User currentUser = em.find(User.class, uid);
-		if (currentUser == null) {
-			LOG.info("User with id {} not found in DB, we'll try to create it", uid);
-			return create(user, token);
-		}
+    /**
+     * setUserApps
+     * <p>
+     * Resolve and set the applications for a user from a set of IDs.
+     * Validates each id exists in DB.
+     *
+     * @param user     Target user entity.
+     * @param appsIds  Application ids to assign (nullable/empty allowed).
+     * @param em       EntityManager.
+     * @throws SeCurisException if any of the referenced applications does not exist.
+     */
+    private void setUserApps(User user, Set<Integer> appsIds, EntityManager em) throws SeCurisException {
+        Set<Application> apps = null;
+        if (appsIds != null && !appsIds.isEmpty()) {
+            apps = new HashSet<>();
+            for (Integer appId : appsIds) {
+                Application o = em.find(Application.class, appId);
+                if (o == null) {
+                    LOG.error("User application with id {} not found in DB", appId);
+                    throw new SeCurisException("User's application not found with ID: " + appId);
+                }
+                apps.add(o);
+            }
+        }
+        user.setApplications(apps);
+    }
 
-		try {
-			this.setUserOrgs(currentUser, user.getOrgsIds(), em);
-		} catch (SeCurisException e) {
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
-		}
-		try {
-			this.setUserApps(currentUser, user.getAppsIds(), em);
-		} catch (SeCurisException e) {
-			return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
-		}
-		currentUser.setFirstName(user.getFirstName());
-		currentUser.setLastName(user.getLastName());
-		currentUser.setRoles(user.getRoles());
-		currentUser.setLang(user.getLang());
-		currentUser.setModificationTimestamp(new Date());
-		if (user.getPassword() != null && !"".equals(user.getPassword())) {
-			currentUser.setPassword(Utils.sha256(user.getPassword()));
-		} else {
-			// Password has not been modified
-			// return
-		}
+    /**
+     * modify
+     * <p>
+     * Update an existing user. If the user does not exist, delegates to {@link #create(User, String)}.
+     * Password is updated only if a non-empty password is provided.
+     * Organizations & applications are fully replaced with the given ids.
+     *
+     * Security: ADMIN only.
+     * Transaction: yes (via @EnsureTransaction).
+     *
+     * @param user  Incoming user payload.
+     * @param uid   Username (path param) to update.
+     * @param token Security token header (unused here).
+     * @return 200 OK with updated user; 404 if reference entities are missing.
+     */
+    @PUT
+    @POST
+    @Path("/{uid}")
+    @EnsureTransaction
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response modify(User user, @PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+        LOG.info("Modifying user with id: {}", uid);
 
-		currentUser.setLastLogin(user.getLastLogin());
+        User currentUser = em.find(User.class, uid);
+        if (currentUser == null) {
+            LOG.info("User with id {} not found in DB, we'll try to create it", uid);
+            return create(user, token);
+        }
 
-		em.persist(currentUser);
-		clearUserCache(currentUser.getUsername());
+        try {
+            this.setUserOrgs(currentUser, user.getOrgsIds(), em);
+        } catch (SeCurisException e) {
+            return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+        }
+        try {
+            this.setUserApps(currentUser, user.getAppsIds(), em);
+        } catch (SeCurisException e) {
+            return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+        }
 
-		return Response.ok(currentUser).build();
-	}
+        currentUser.setFirstName(user.getFirstName());
+        currentUser.setLastName(user.getLastName());
+        currentUser.setRoles(user.getRoles());
+        currentUser.setLang(user.getLang());
+        currentUser.setModificationTimestamp(new Date());
 
-	@DELETE
-	@Path("/{uid}")
-	@EnsureTransaction
-	@Produces({ MediaType.APPLICATION_JSON })
-	@Securable(roles = Rol.ADMIN)
-	@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
-	public Response delete(@PathParam("uid") String uid, @Context HttpServletRequest request) {
-		LOG.info("Deleting app with id: {}", uid);
-		// EntityManager em = emProvider.get();
-		User user = em.find(User.class, uid);
-		if (user == null) {
-			LOG.error("User with id {} can not be deleted, It was not found in DB", uid);
-			return Response.status(Status.NOT_FOUND).build();
-		}
+        // Optional password update
+        if (user.getPassword() != null && !"".equals(user.getPassword())) {
+            currentUser.setPassword(Utils.sha256(user.getPassword()));
+        }
 
-		em.remove(user);
-		clearUserCache(user.getUsername());
-		return Response.ok(Utils.createMap("success", true, "id", uid)).build();
-	}
+        // lastLogin can be set through API (rare), otherwise managed at login
+        currentUser.setLastLogin(user.getLastLogin());
 
-	private void clearUserCache(String username) {
-		cache.remove("roles_" + username);
-		cache.remove("orgs_" + username);
-	}
+        em.persist(currentUser);
+        clearUserCache(currentUser.getUsername());
 
-	@POST
-	@Path("/login")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response login(@FormParam("username") String username, @FormParam("password") String password, @Context HttpServletRequest request) throws SeCurisServiceException {
-		LOG.info("index session: " + request.getSession());
+        return Response.ok(currentUser).build();
+    }
 
-		// EntityManager em = emProvider.get();
-		User user = em.find(User.class, username);
-		if (user == null) {
-			LOG.error("Unknown username {} used in login service", username);
-			throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
-		}
-		String securedPassword = Utils.sha256(password);
+    /**
+     * delete
+     * <p>
+     * Delete a user by username.
+     *
+     * Security: ADMIN only.
+     * Transaction: yes (via @EnsureTransaction).
+     *
+     * @param uid Username to delete.
+     * @param request Http servlet request (unused).
+     * @return 200 OK on success; 404 if user does not exist.
+     */
+    @DELETE
+    @Path("/{uid}")
+    @EnsureTransaction
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Securable(roles = Rol.ADMIN)
+    @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+    public Response delete(@PathParam("uid") String uid, @Context HttpServletRequest request) {
+        LOG.info("Deleting app with id: {}", uid);
 
-		if (securedPassword == null || !securedPassword.equals(user.getPassword())) {
-			throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
-		}
-		user.setLastLogin(new Date());
-		em.getTransaction().begin();
-		try {
-			em.persist(user);
-			em.getTransaction().commit();
-		} catch (PersistenceException ex) {
-			LOG.error("Error updating last login date for user: {}", username);
-			LOG.error(ex);
-			em.getTransaction().rollback();
-		}
-		clearUserCache(username);
-		String userFullName = String.format("%s %s", user.getFirstName(), user.getLastName() == null ? "" : user.getLastName()).trim();
-		String tokenAuth = tokenHelper.generateToken(username);
-		return Response.ok(Utils.createMap("success", true, "token", tokenAuth, "username", username, "full_name", userFullName)).build();
-	}
+        User user = em.find(User.class, uid);
+        if (user == null) {
+            LOG.error("User with id {} can not be deleted, It was not found in DB", uid);
+            return Response.status(Status.NOT_FOUND).build();
+        }
 
-	/**
-	 * Check if current token is valid
-	 * 
-	 * @param user
-	 * @param password
-	 * @param request
-	 * @return
-	 */
-	@POST
-	@Path("/check")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
-		if (token == null) {
-			token = token2;
-		}
-		if (token == null) {
-			return Response.status(Status.FORBIDDEN).build();
-		}
+        em.remove(user);
+        clearUserCache(user.getUsername());
+        return Response.ok(Utils.createMap("success", true, "id", uid)).build();
+    }
 
-		LOG.info("Token : " + token);
-		String user = tokenHelper.extractUserFromToken(token);
-		LOG.info("Token user: " + user);
-		Date date = tokenHelper.extractDateCreationFromToken(token);
-		LOG.info("Token date: " + date);
-		boolean valid = tokenHelper.isTokenValid(token);
+    /**
+     * clearUserCache
+     * <p>
+     * Helper to invalidate cached role/org projections after changes.
+     *
+     * @param username The user whose cache entries must be cleared.
+     */
+    private void clearUserCache(String username) {
+        cache.remove("roles_" + username);
+        cache.remove("orgs_" + username);
+    }
 
-		LOG.info("Is Token valid: " + valid);
+    // ---------------------------------------------------------------------
+    // Auth helpers
+    // ---------------------------------------------------------------------
 
-		return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
-	}
+    /**
+     * login
+     * <p>
+     * Validates username & password against stored SHA-256 hash. On success,
+     * updates lastLogin timestamp, clears cache and returns an auth token.
+     *
+     * Token format: Base64("<secret> <user> <ISO8601-date>")
+     * where secret = SHA-256(seed + user + date).
+     *
+     * @param username Plain username.
+     * @param password Plain password (SHA-256 will be computed server-side).
+     * @param request  Http request, used to log underlying session (not required for token flow).
+     * @return 200 OK with {token, username, full_name}; 401 on invalid credentials.
+     * @throws SeCurisServiceException if user is missing or password mismatch.
+     */
+    @POST
+    @Path("/login")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Response login(@FormParam("username") String username, @FormParam("password") String password, @Context HttpServletRequest request) throws SeCurisServiceException {
+        LOG.info("index session: " + request.getSession());
 
-	@GET
-	@Path("/logout")
-	@Produces({ MediaType.APPLICATION_JSON })
-	public Response logout(@Context HttpServletRequest request) {
-		request.getSession().invalidate();
-		return Response.ok().build();
-	}
+        User user = em.find(User.class, username);
+        if (user == null) {
+            LOG.error("Unknown username {} used in login service", username);
+            throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
+        }
+        String securedPassword = Utils.sha256(password);
+
+        if (securedPassword == null || !securedPassword.equals(user.getPassword())) {
+            throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
+        }
+
+        user.setLastLogin(new Date());
+        em.getTransaction().begin();
+        try {
+            em.persist(user);
+            em.getTransaction().commit();
+        } catch (PersistenceException ex) {
+            LOG.error("Error updating last login date for user: {}", username);
+            LOG.error(ex);
+            em.getTransaction().rollback();
+        }
+
+        clearUserCache(username);
+        String userFullName = String.format("%s %s", user.getFirstName(), user.getLastName() == null ? "" : user.getLastName()).trim();
+        String tokenAuth = tokenHelper.generateToken(username);
+        return Response.ok(Utils.createMap("success", true, "token", tokenAuth, "username", username, "full_name", userFullName)).build();
+    }
+
+    /**
+     * check
+     * <p>
+     * Validates a token and echoes token claims (user, creation date, token string).
+     * Accepts header or query param for convenience.
+     *
+     * @param token  Token in header {@link TokenHelper#TOKEN_HEADER_PÀRAM}, may be null.
+     * @param token2 Token in query param 'token', used if header is null.
+     * @return 200 OK with {valid, user, date, token} or 403 if token missing.
+     */
+    @POST
+    @Path("/check")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+        if (token == null) {
+            token = token2;
+        }
+        if (token == null) {
+            return Response.status(Status.FORBIDDEN).build();
+        }
+
+        LOG.info("Token : " + token);
+        String user = tokenHelper.extractUserFromToken(token);
+        LOG.info("Token user: " + user);
+        Date date = tokenHelper.extractDateCreationFromToken(token);
+        LOG.info("Token date: " + date);
+        boolean valid = tokenHelper.isTokenValid(token);
+
+        LOG.info("Is Token valid: " + valid);
+
+        return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
+    }
+
+    /**
+     * logout
+     * <p>
+     * Invalidates the HTTP session (useful if the UI also tracks session).
+     * Note: token-based auth is stateless; tokens are not revoked here.
+     *
+     * @param request HttpServletRequest to invalidate session.
+     * @return 200 OK always.
+     */
+    @GET
+    @Path("/logout")
+    @Produces({ MediaType.APPLICATION_JSON })
+    public Response logout(@Context HttpServletRequest request) {
+        request.getSession().invalidate();
+        return Response.ok().build();
+    }
 }
+

--
Gitblit v1.3.2