FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index mod_harbour mod_harbour.v2.1 and RESTful API
Posts: 129
Joined: Mon Oct 17, 2005 03:03 AM
mod_harbour.v2.1 and RESTful API
Posted: Thu Feb 05, 2026 02:33 AM

Using mod_harbour.v2.1 to implement a RESTful API call

  • Since mod_harbour.v2.1 does not support URLs like https://xxx.xxx.xxx/api.prg/user/1, you need to add this functionality manually.
  • Modify mod_harbour.v2.1/source/ap_func_c.c and add the AP_GETURI() function:
    //----------------------------------------------------------------//
    // mh_GetUri() cannot retrieve URLs like: http?://xxx.xxx/api.prg/user/1
    // So we add this function to get the full URI
    HB_FUNC(AP_GETURI)
    {
       hb_retc(GetRequestRec()->uri);
    }
    b. Because JWT is required, download the file from: https://github.com/matteobaccan/HarbourJwt — you only need the jwt.prg file.
    c. Modify ./mod_harbour.v2.1/libmhapache.hbp and add: \your_path\jwt.prg
    d. Modify ./mod_harbour.v2.1/source/externs.hbx and add: extern AP_GETURI
    e. Recompile to generate libmhapache.dll and mod_harbour.v2.so
  • Modify the Apache httpd.conf configuration:
    You need to set two environment variables:
        SetEnv JWT_SECRET "your_sha256"
        SetEnv JWT_REFRESH_SECRET "your_sha256"

JWT_SECRET: Access Token, used to validate access requests
JWT_REFRESH_SECRET: Refresh Token, used to obtain a new Access Token when the old one expires
If your website manages multiple RESTful APIs at the same time, you can separate them using <Directory> blocks, for example:

<Directory "{DOCUMENTPATH}/api">
    SetEnv JWT_SECRET "...."
    SetEnv JWT_REFRESH_SECRET "...."
</Directory>

<Directory "{DOCUMENTPATH}/zzz">
    SetEnv JWT_SECRET "...."
    SetEnv JWT_REFRESH_SECRET "...."
</Directory>

To generate the SHA256 values, use the following command and paste the result into JWT_SECRET and JWT_REFRESH_SECRET:

    openssl rand -base64 32

Next is the [Frontend] test webpage content:

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF--8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>mod_harbour RESTful API 測試</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; padding: 20px; max-width: 800px; margin: auto; }
        h1, h2 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
        .container { display: flex; gap: 20px; }
        .controls, .output { flex: 1; min-width: 0; /* 解決 flex 項目被內容撐開的問題 */ }
        button { display: block; width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; background-color: #f0f0f0; cursor: pointer; }
        button:hover { background-color: #e0e0e0; }
        input { width: calc(100% - 22px); padding: 10px; margin-bottom: 10px; }
        pre { background-color: #2d2d2d; color: #f1f1f1; padding: 15px; border-radius: 5px; white-space: pre; overflow-x: auto; }
        label { font-weight: bold; margin-bottom: 5px; display: block; }
    </style>
</head>
<body>

<h1>mod_harbour RESTful API 測試</h1>

<div class="container">
    <div class="controls">
        <h2>控制面板</h2>

        <button onclick="getAllUsers()">GET /users (取得所有使用者)</button>

        <hr>
        <label for="userId">使用者 ID:</label>
        <input type="text" id="userId" placeholder="輸入 ID (例如: 1)" value="1">
        <button onclick="getUserById()">GET /users/{id} (取得單一使用者)</button>
        <button onclick="deleteUser()">DELETE /users/{id} (刪除使用者)</button>

        <hr>
        <label for="userName">姓名:</label>
        <input type="text" id="userName" placeholder="輸入姓名">
        <label for="userEmail">Email:</label>
        <input type="text" id="userEmail" placeholder="輸入 Email">
        <button onclick="createUser()">POST /users (新增使用者)</button>
        <button onclick="updateUser()">PUT /users/{id} (更新使用者)</button>

        <hr>
        <h2>身分驗證 (JWT)</h2>
        <label for="loginUser">使用者名稱:</label>
        <input type="text" id="loginUser" value="admin">
        <label for="loginPass">密碼:</label>
        <input type="password" id="loginPass" value="password">
        <button onclick="login()">POST /login (登入並取得 Token)</button>

        <button onclick="getProfile()">GET /profile (測試保護路由)</button>
        <button onclick="logout()">登出 (清除 Token)</button>

    </div>
    <div class="output">
        <h2>輸出結果</h2>
        <pre id="response">點擊按鈕以發送請求...</pre>
    </div>
</div>

<script>
    // API 的基礎 URL,指向 .prg 原始碼檔案
    const API_BASE_URL = '/RESTful/api.prg';
    // 頁面載入時,嘗試從 localStorage 讀取 token
    let jwtToken = localStorage.getItem('jwtToken'); 

    // 如果一開始就有 token,可以提示使用者
    if (jwtToken) {
        console.log('偵測到已儲存的 Token,目前為登入狀態。');
    }

    const responseElement = document.getElementById('response');

    // 新增:嘗試刷新 Access Token 的函式
    async function tryRefreshToken() {
        try {
            // 注意:這裡不需要手動帶入 headers,因為瀏覽器會自動帶上 HttpOnly cookie
            const response = await fetch(API_BASE_URL + '/refresh_token', { method: 'POST' });

            if (!response.ok) {
                return false;
            }
            const data = await response.json();
            if (data.token) {
                jwtToken = data.token;
                localStorage.setItem('jwtToken', jwtToken);
                return true;
            }
            return false;
        } catch (error) {
            console.error('Error during token refresh:', error);
            return false;
        }
    }

    async function apiCall(method, path, body = null) {
        const url = API_BASE_URL + path;
        const options = {
            method: method,
            headers: {}
        };

        options.headers['Content-Type'] = 'application/json';
        if (jwtToken) {
            options.headers['Authorization'] = `Bearer ${jwtToken}`;
        };

        if (body) {
            options.body = JSON.stringify(body);
        }

        try {
            responseElement.textContent = `正在發送 ${method} 請求至 ${url} ...`;
            let response = await fetch(url, options);

            // 新增:如果收到 401 (Unauthorized),嘗試刷新 token 並重試
            if (response.status === 401 && path !== '/login' && path !== '/refresh_token') {
                console.log('Access Token 可能已過期,正在嘗試刷新...');
                const refreshSuccess = await tryRefreshToken();

                if (refreshSuccess) {
                    console.log('Token 刷新成功,正在重試原始請求...');
                    // 使用新的 token 更新 Authorization 標頭
                    options.headers['Authorization'] = `Bearer ${jwtToken}`;
                    // 重試請求
                    response = await fetch(url, options);
                } else {
                    console.log('Token 刷新失敗,請重新登入。');
                    alert('您的連線階段已過期,請重新登入。');
                    await logout(); // 清理前端和後端的登入狀態
                    return;
                }
            }

            let responseText = `狀態: ${response.status} ${response.statusText}\n\n`;
            
            // 處理沒有內容的回應 (例如 204 No Content)
            if (response.status === 204) {
                responseText += "操作成功,無返回內容。";
            } else {
                // 如果是登入成功,儲存新的 access token
                if (path === '/login' && response.ok) {
                    const data = await response.json();
                    localStorage.setItem('jwtToken', data.token); // 將 Token 儲存到 localStorage
                    jwtToken = data.token;
                    alert('登入成功!Access Token 已儲存。');
                    responseText += JSON.stringify(data, null, 2);
                    responseElement.textContent = responseText;
                    return; // 結束函式
                }

                const data = await response.json();
                responseText += JSON.stringify(data, null, 2);
            }
            
            responseElement.textContent = responseText;

        } catch (error) {
            responseElement.textContent = `發生錯誤:\n${error}`;
        }
    }

    function getAllUsers() {
        apiCall('GET', '/users');
    }

    function getUserById() {
        const id = document.getElementById('userId').value;
        if (!id) {
            alert('請輸入使用者 ID');
            return;
        }
        apiCall('GET', `/users/${id}`);
    }

    function createUser() {
        const name = document.getElementById('userName').value;
        const email = document.getElementById('userEmail').value;

        if (!name || !email) {
            alert('請輸入姓名和 Email');
            return;
        }

        const body = { name, email };
        apiCall('POST', '/users', body);
    }

    function updateUser() {
        const id = document.getElementById('userId').value;
        const name = document.getElementById('userName').value;
        const email = document.getElementById('userEmail').value;

        if (!id) {
            alert('請輸入要更新的使用者 ID');
            return;
        }

        if (!name && !email) {
            alert('請至少輸入姓名或 Email 來進行更新');
            return;
        }

        const body = {};
        if (name) body.name = name;
        if (email) body.email = email;

        apiCall('PUT', `/users/${id}`, body);
    }

    function deleteUser() {
        const id = document.getElementById('userId').value;
        if (!id) {
            alert('請輸入要刪除的使用者 ID');
            return;
        }
        if (confirm(`確定要刪除 ID 為 ${id} 的使用者嗎?`)) {
            apiCall('DELETE', `/users/${id}`);
        }
    }

    function login() {
        const username = document.getElementById('loginUser').value;
        const password = document.getElementById('loginPass').value;

        if (!username || !password) {
            alert('請輸入使用者名稱和密碼');
            return;
        }

        const body = { username, password };
        jwtToken = null; // 登入前先清除記憶體中的舊 token
        localStorage.removeItem('jwtToken'); // 同時清除 localStorage 中的舊 token
        apiCall('POST', '/login', body);
    }

    function getProfile() {
        if (!jwtToken) {
            alert('請先登入以取得 Token!');
            return;
        }
        apiCall('GET', '/profile');
    }

    async function logout() {
        // 先呼叫後端 API 來清除 HttpOnly cookie
        await apiCall('POST', '/logout');

        // 然後清理前端的 token
        jwtToken = null;
        localStorage.removeItem('jwtToken');
        alert('已登出成功。');
    }

</script>

</body>
</html>

Next is the [Backend] main program:

<?prg
/*
 * api.prg (範例程式)
 * 這是一個使用 mod_harbour v2.1 開發的簡單 RESTful API 範例 (物件導向版本)。
 *
 * 這個版本將原來的程序化程式碼重構為一個名為 UserApi 的類別,
 * 使得程式碼結構更清晰、更易於維護和擴展。
 *
 * 主要功能:
 * - 管理 "users" 資源,提供 CRUD 操作。
 * - 使用 JWT (JSON Web Token) 進行身分驗證。
 * - 實現 Refresh Token 機制以延長使用者登入狀態。
 * - 採用物件導向設計,將所有相關邏輯封裝在 UserApi 類別中。
 */
#include "hbclass.ch"
// --- 主程序 ---
PROCEDURE Main()

   // 取得當前請求的 HTTP 方法 (GET, POST, PUT, DELETE)。
   LOCAL cMethod  := AP_Method()
   // 使用我們在 C 原始碼中新增的 AP_GetUri() 函式
   LOCAL hHeaders := AP_HeadersIn()
   LOCAL cURI     := AP_GetUri() // 請求的 URL 路徑,例如 /RESTful/api.prg/users/1
   LOCAL cBody    := AP_GetBody() // 預先讀取 body,避免在多個方法中重複讀取
   LOCAL oApi

   // 建立 API 物件並傳入請求資訊
   oApi := UserApi():New( cMethod, cURI, hHeaders, cBody )

   // 執行請求處理
   oApi:HandleRequest()

RETURN

// --- API 類別定義 ---
CLASS UserApi

   // --- 資料成員 (Properties) ---
   // 模擬資料庫
   DATA aUsers          INIT {}
   // 檔案快取時間戳
   DATA tsLastModTime     INIT NIL

   // JWT 密鑰
   DATA cJwtSecret        INIT ""
   DATA cJwtRefreshSecret INIT ""

   // 請求相關資料
   DATA cMethod         INIT ""
   DATA cURI            INIT ""
   DATA hHeaders        INIT NIL
   DATA cBody           INIT ""

   // --- 方法 (Methods) ---
   METHOD New( cMethod, cURI, hHeaders, cBody )
   METHOD HandleRequest()

   // API 端點方法
   METHOD GetAllUsers()
   METHOD GetUser( nID )
   METHOD CreateUser()
   METHOD UpdateUser( nID )
   METHOD DeleteUser( nID )
   METHOD Login()
   METHOD GetProfile()
   METHOD RefreshToken()
   METHOD Logout()

   // 輔助方法
   METHOD VerifyToken() PROTECTED
   METHOD SendError( nStatusCode, cMessage ) PROTECTED
   METHOD FindUser( nID ) PROTECTED
   METHOD FindUserIndex( nID ) PROTECTED
   METHOD GetNextID() PROTECTED
   METHOD LoadData() PROTECTED
   METHOD SaveData() PROTECTED
   METHOD InitData() PROTECTED
   METHOD GetCookieValue( cCookieHeader, cCookieName ) PROTECTED

ENDCLASS
// --- 類別實作 ---
....
....
?>

Important!! Please reconfigure your own Access Token and Refresh Token lifecycle!!

You can view full code from:https://www4.zzz.com.tw/phpBB3/viewtopic.php?f=2&t=370

line ID: ssbbstw

WeChat ID: ssbbstw
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: mod_harbour.v2.1 and RESTful API
Posted: Fri Feb 06, 2026 03:39 AM

Good evening..

could you show us how to compile with msvc64 2019 or 2022.. and the link to download the version of Harbour according to msvc64..

JONSSON RUSSI

Posts: 129
Joined: Mon Oct 17, 2005 03:03 AM
Re: mod_harbour.v2.1 and RESTful API
Posted: Sat Feb 07, 2026 03:36 AM

I am using MSVC64 with VS Studio 2019
Method to compile mod_harbour.v2.1:
1.Download the project from GitHub
2.Run the file inside named go64.bat

After execution, it will generate:
libmhapache.dll
mod_harbour.v2.so

russimicro wrote:

could you show us how to compile with msvc64 2019 or 2022.. and the link to download the version of Harbour according to msvc64..

line ID: ssbbstw

WeChat ID: ssbbstw
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: mod_harbour.v2.1 and RESTful API
Posted: Sat Feb 07, 2026 03:59 AM

Good evening.. I managed to compile partially... it gives me these errors... function CURL_EASY_ER_BUFF_GET() ; return function CURL_MULTI_ADD_HANDLE() ; return function CURL_MULTI_CLEANUP() ; return function CURL_MULTI_INFO_READ() ; return function CURL_MULTI_INIT() ; return function CURL_MULTI_PERFORM() ; return function CURL_MULTI_POLL() ; return function CURL_MULTI_REMOVE_HANDLE() ; return function CURL_WS_RECV() ; return function CURL_WS_SEND() ; return function BIO_GET_CONN_ADDRESS() ; return function SSL_CTX_USE_RSAPRIVATEKEY() ; return ...

I defined them ... to bypass the error.

But the libmhapache.dll generates an error... Is it possible for you to share the mod_harbour and the Harbour version you use..

Thanks

Posts: 129
Joined: Mon Oct 17, 2005 03:03 AM
Re: mod_harbour.v2.1 and RESTful API
Posted: Sat Feb 07, 2026 04:08 AM

disable: #{allmsvc}hbcurl.hbc

add: #[hbcurl] {msvc64}-lhbcurl

old libcurl is: {msvc64}-llibcurl-x64

my libcurl is: #[curl] {msvc64}-llibcurl

line ID: ssbbstw

WeChat ID: ssbbstw
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: mod_harbour.v2.1 and RESTful API
Posted: Sat Feb 07, 2026 12:23 PM

-hbdynvm
{msvc64}-ooutput/win64/libmhapache
{msvc}-ooutput/win32/libmhapache
{gcc}-o./output/linux/libmhapache.so
{linux|darwin}-static

-mt
-trace
-info
-rebuild

-l-hbfship
-l-hbxpp

{msvc}-llibssl-1_1
{msvc}-llibcrypto-1_1
{msvc64}-llibcrypto-1_1-x64
{msvc64}-llibssl-1_1-x64
#{msvc64}-llibcurl-x64
{msvc}-llibcurl
{msvc64}-lhbcurl
{msvc64}-llibcurl

{allmsvc}-lapr-1
{allmsvc}-laprutil-1
{allmsvc}-llibapr-1
{allmsvc}-llibaprutil-1
{allmsvc}-llibhttpd

-Ld:\xampp\apache\lib

-id:\xampp\apache\include
source/mh_apache.prg
source/ap_func.prg
source/ap_func_c.c
source/cookies.prg
source/legacy_func.prg
source/legacy_func_c.c
source/main.prg
source/persistence.prg
source/prepro.prg
source/preproPHP.prg
source/trace.prg
source/errorsys.prg
source/sessions.prg
source/jwt.prg

{allmsvc}-cflag=-Id:\xampp\apache\include
{gcc}-cflag=-I/usr/include/apache2
{gcc}-cflag+=-I/usr/include/apr-1.0
{allmsvc}-id:\xampp\apache\include
{darwin}-cflag=-I/usr/include/apache2
{darwin}-cflag+=-I/usr/include/apr-1.0
{darwin}-cflag+=-I/usr/local/Cellar/httpd/2.4.47/include/httpd
{darwin}-cflag+=-I/usr/local/Cellar/apr-util/1.6.1_3/libexec/include/apr-1
{darwin}-cflag+=-I/usr/local/Cellar/apr/1.7.0_2/libexec/include/apr-1

{allmsvc}hbwin.hbc
{allmsvc}hbhpdf.hbc
{allmsvc}xhb.hbc
{allmsvc}hbct.hbc
#{allmsvc}hbcurl.hbc
{allmsvc}hbmzip.hbc
{allmsvc}hbziparc.hbc
{allmsvc}hbmemio.hbc
{allmsvc}hbnetio.hbc
{allmsvc}hbssl.hbc
{allmsvc}hbmisc.hbc

{gcc}-dflag=@libs.txt
{darwin}-dflag=@libs_osx.txt

{darwin}-FCoreFoundation
{darwin}-FSecurity

//********************************************

These lib...
libcrypto-1_1-x64.lib
libssl-1_1-x64.lib
it can't find them...
I took them from C:harbour_binaries-masterWindowsharbour_windows_64.ziplibwinmsvc64
and these I downloaded from the web...
ssleay32.lib
libeay32.lib

//*************************************************

error :
libhpdf.lib(hpdfpdfa.obj) : error LNK2019: s¡mbolo externo impctime64 sin resolver al que se hace referencia en la funci¢n HPDF_PDFA_GenerateID
libhpdf.lib(hpdfpdfa.obj) : error LNK2019: s¡mbolo externo imptime64 sin resolver al que se hace referencia en la funci¢n HPDF_PDFA_GenerateID
...
...
...

It seems like it might be the Harbour version for msv64.. could you share it..?

Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: mod_harbour.v2.1 and RESTful API
Posted: Sat Feb 07, 2026 02:00 PM

I also need to include the TDOLPHIN and HBZEBRA libraries

Posts: 129
Joined: Mon Oct 17, 2005 03:03 AM
Re: mod_harbour.v2.1 and RESTful API
Posted: Sat Feb 07, 2026 03:18 PM

about PDF: I only use: {allmsvc}hbhpdf.hbc no need "libhpdf.lib"

about SSL: ssleay32.lib libeay32.lib is openssl 1.0.2(u) I'm use v3.4.1.

What library is need about you?

line ID: ssbbstw

WeChat ID: ssbbstw
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Re: mod_harbour.v2.1 and RESTful API
Posted: Sun Feb 08, 2026 01:08 AM

Good evening... S

ince I am starting with mod_harbour, could you provide the mod_harbour and the harbour you use... and if they are the latest versions, even better. I also need to be able to include the dolphin library.

Thanks

JONSSON RUSSI

Posts: 129
Joined: Mon Oct 17, 2005 03:03 AM
Re: mod_harbour.v2.1 and RESTful API
Posted: Sun Feb 08, 2026 03:56 AM

You’d better learn to compile on your own, or it’ll be a hassle when you need to fix issues!
If you run into any issues, feel free to bring them up for discussion.

russimicro wrote:

Good evening... S

ince I am starting with mod_harbour, could you provide the mod_harbour and the harbour you use... and if they are the latest versions, even better. I also need to be able to include the dolphin library.

line ID: ssbbstw

WeChat ID: ssbbstw

Continue the discussion