diff --git a/doc/images/zoho-001.png b/doc/images/zoho-001.png
new file mode 100644
index 0000000..9610fc6
Binary files /dev/null and b/doc/images/zoho-001.png differ
diff --git a/doc/images/zoho-002.png b/doc/images/zoho-002.png
new file mode 100644
index 0000000..7dbbc71
Binary files /dev/null and b/doc/images/zoho-002.png differ
diff --git a/doc/zoho/README.md b/doc/zoho/README.md
index d0cbf31..71dab2a 100644
--- a/doc/zoho/README.md
+++ b/doc/zoho/README.md
@@ -41,3 +41,50 @@ ee9ba49f16e77c64abb89cea13542d2326d8d03ce8
## 3. Generate Access and Refresh Token
https://accounts.zoho.com/oauth/v2/token?code=1000.dd7exxxxxxxxxxxxxxxxxxxxxxxx9bb8.b6c0xxxxxxxxxxxxxxxxxxxxxxxxdca4&client_id=1000.NLTM2KUEHI696MSLGEIANATVGGX7IN&client_secret=ee9ba49f16e77c64abb89cea13542d2326d8d03ce8&redirect_uri=http://www.zoho.com/books&grant_type=authorization_code
+
+# Self Client
+
+Man kann bei zoho mit einem sogenannten 'Self Client' arbeiten. Dieser erlaubt es ohne Redirect auf die Zoho Webseite einen Code anzufordern:
+
+## 1. Self Client einrichten
+
+Auf die API COnsole von Zoho zugreifen:
+
+https://api-console.zoho.eu/client/1000.GR5FTPZYB06HEWSFSSX4DJU3OBD4EJ
+
+
+
+Es kann maximal ein Client eingerichtet werden
+
+Unter Scope trägt man dann den gewünschten Scope ein. Z.B.:
+
+ ZohoBooks.invoices.CREATE,ZohoBooks.invoices.READ,ZohoBooks.invoices.UPDATE,ZohoBooks.invoices.DELETE
+
+und eine Description
+
+
+
+Den generierten Code nun in Anwendung übertragen und innerhalb (!!) der 'Time Duration' den Access und Refresh Token generieren!
+
+Siehe Junit Test `TestUpdateTokens`
+
+Find details also [here](https://www.zoho.com/writer/help/api/v1/oauth-step2.html#self-client)
+
+## Example for JUnit Test
+
+Der Junit Test `TestUpdateTokens` zeigt das Prinzip
+
+```
+client ID= 1000.GR5FTPZYB06HEWSFSSX4DJU3OBD4EJ
+client secret= 3f3e74c5212bbc924a7ebf77f887c6296db1106c47
+```
+
+In der Web GUi einen neuen Code generieren:
+
+Scope=ZohoBooks.invoices.CREATE,ZohoBooks.invoices.READ,ZohoBooks.invoices.UPDATE,ZohoBooks.invoices.DELETE
+
+Danach den Code token kopieren:
+
+1000.7fe6315e8848b92a2f281b885677d37a.428c02dc2309606c86271842f1f67de4
+
+**Note:** Der Code muss für Test immer wieder neu erstellt werden, da er nur für einige Minuten gültig ist!
diff --git a/doc/zoho/image.png b/doc/zoho/image.png
new file mode 100644
index 0000000..d0a4e76
Binary files /dev/null and b/doc/zoho/image.png differ
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/TokenResponse.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/TokenResponse.java
new file mode 100644
index 0000000..66cb23e
--- /dev/null
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/TokenResponse.java
@@ -0,0 +1,74 @@
+package com.alexanderlogistics.zoho;
+
+/**
+ * Helper class for JSON deserialization
+ *
+ * If the request is successful, you would receive the following:
+ *
+ *
+ {
+ "access_token": "{access_token}",
+ "refresh_token": "{refresh_token}",
+ "api_domain": "https://www.zohoapis.com",
+ "token_type": "Bearer",
+ "expires_in": 3600
+ }
+ *
+ */
+public class TokenResponse {
+ private String access_token;
+ private String refresh_token;
+ private long expires_in;
+ private String api_domain;
+ private String token_type;
+ private String scope; // Neu hinzugefügt für dein Response-Feld
+
+ // Getter/Setter notwendig für Yasson
+ public String getAccess_token() {
+ return access_token;
+ }
+
+ public void setAccess_token(String access_token) {
+ this.access_token = access_token;
+ }
+
+ public String getRefresh_token() {
+ return refresh_token;
+ }
+
+ public void setRefresh_token(String refresh_token) {
+ this.refresh_token = refresh_token;
+ }
+
+ public long getExpires_in() {
+ return expires_in;
+ }
+
+ public void setExpires_in(long expires_in) {
+ this.expires_in = expires_in;
+ }
+
+ public String getApi_domain() {
+ return api_domain;
+ }
+
+ public void setApi_domain(String api_domain) {
+ this.api_domain = api_domain;
+ }
+
+ public String getToken_type() {
+ return token_type;
+ }
+
+ public void setToken_type(String token_type) {
+ this.token_type = token_type;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+}
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java
new file mode 100644
index 0000000..1be7114
--- /dev/null
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java
@@ -0,0 +1,40 @@
+package com.alexanderlogistics.zoho;
+
+import jakarta.annotation.security.DeclareRoles;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.annotation.security.RunAs;
+import jakarta.ejb.Lock;
+import jakarta.ejb.LockType;
+import jakarta.ejb.Singleton;
+
+/**
+ * Der ZohoAPIService stellt methoden für den Zugriff auf die Zoho API bereit.
+ * Er handelt auch die Authentifizeirung über einen Access Token. Der
+ * ZohoOAuhtGrantService der dazu dient einen AccessCode von Zoho zu ermitteln
+ * übergibt den code an diesesn Servcie. Er dietn also als Singelton
+ */
+@DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
+ "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
+ "org.imixs.ACCESSLEVEL.MANAGERACCESS" })
+@RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
+ "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
+ "org.imixs.ACCESSLEVEL.MANAGERACCESS" })
+@Singleton
+@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
+public class ZohoAPIService {
+
+ private String accessToken;
+ private String clientID;
+ private String clientSecret;
+
+ @Lock(LockType.WRITE) // Standardmäßig ist @Lock(LockType.READ)
+ public void setAccessToken(String token) {
+ this.accessToken = token;
+ }
+
+ @Lock(LockType.READ) // Erlaubt parallele Lesezugriffe
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+}
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthGrantService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthGrantService.java
index fb1e1f1..33c5544 100644
--- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthGrantService.java
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthGrantService.java
@@ -32,11 +32,9 @@ import java.util.logging.Logger;
import org.imixs.workflow.exceptions.QueryException;
-import jakarta.annotation.Resource;
-import jakarta.ejb.SessionContext;
+import jakarta.ejb.EJB;
import jakarta.ejb.Stateless;
import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@@ -69,11 +67,8 @@ public class ZohoOAuthGrantService {
@Context
private HttpServletRequest servletRequest;
- @Context
- HttpServletResponse servletResponse;
-
- @Resource
- private SessionContext ctx;
+ @EJB
+ ZohoOAuthManager zohoOAuthManager;
private static Logger logger = Logger.getLogger(ZohoOAuthGrantService.class.getName());
@@ -122,6 +117,8 @@ public class ZohoOAuthGrantService {
logger.info("│ ├── location=" + location);
logger.info("│ ├── accounts-server=" + accountsServer);
+ zohoOAuthManager.updateTokens(code);
+
return "Zoho Grant Token received!";
}
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthManager.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthManager.java
new file mode 100644
index 0000000..f918417
--- /dev/null
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoOAuthManager.java
@@ -0,0 +1,149 @@
+package com.alexanderlogistics.zoho;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.imixs.workflow.exceptions.InvalidAccessException;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.security.RunAs;
+import jakarta.ejb.Lock;
+import jakarta.ejb.LockType;
+import jakarta.ejb.Singleton;
+import jakarta.inject.Inject;
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+
+/**
+ * Der ZohoOAuthManager stellt Methoden bereit um einen Access Token zu
+ * erstellen.
+ *
+ *
+ */
+@Singleton
+@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
+public class ZohoOAuthManager {
+
+ private static Logger logger = Logger.getLogger(ZohoOAuthManager.class.getName());
+
+ private String refreshToken;
+ private String accessToken;
+ private long accessTokenExpiry; // Timestamp (System.currentTimeMillis() + expires_in*1000)
+
+ @Inject
+ @ConfigProperty(name = "zoho.client.id", defaultValue = "1000.NLTM2KUEHI696MSLGEIANATVGGX7IN")
+ Optional