/*******************************************************************************
 * Copyright (c) 2025, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include "../../client/client.h"

#include <node.h>
#include <node_object_wrap.h>

namespace authdb_nds {

    using v8::Context;
    using v8::Function;
    using v8::FunctionCallbackInfo;
    using v8::FunctionTemplate;
    using v8::Isolate;
    using v8::Local;
    using v8::Object;
    using v8::String;
    using v8::Value;
    using v8::Exception;
    using v8::Boolean;
    using v8::Persistent;
    using v8::MaybeLocal;

    /*------------------------------------------------------------------------
     *  Helper: extract a UTF-8 std::string from a V8 value
     *----------------------------------------------------------------------*/
    static std::string V8ToString(Isolate* isolate, Local<Value> val) {
        String::Utf8Value utf8(isolate, val);
        return (*utf8) ? std::string(*utf8) : std::string();
    }

    /*========================================================================
     *  AuthDBConnectionJS  –  wraps  authdb::client::ClientConnection
     *======================================================================*/
    class AuthDBConnectionJS : public node::ObjectWrap {
    public:
        static void Init(Local<Object> exports) {
            Isolate* isolate = exports->GetIsolate();
            Local<Context> context = isolate->GetCurrentContext();

            Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
            tpl->SetClassName(String::NewFromUtf8(isolate, "ClientConnection").ToLocalChecked());
            tpl->InstanceTemplate()->SetInternalFieldCount(1);

            NODE_SET_PROTOTYPE_METHOD(tpl, "setUrl",          SetUrl);
            NODE_SET_PROTOTYPE_METHOD(tpl, "setClientName",   SetClientName);
            NODE_SET_PROTOTYPE_METHOD(tpl, "setClientSecret", SetClientSecret);
            NODE_SET_PROTOTYPE_METHOD(tpl, "setUsername",      SetUsername);
            NODE_SET_PROTOTYPE_METHOD(tpl, "setUserPassword", SetUserPassword);
            NODE_SET_PROTOTYPE_METHOD(tpl, "getClientId",     GetClientId);
            NODE_SET_PROTOTYPE_METHOD(tpl, "getSessionID",    GetSessionID);
            NODE_SET_PROTOTYPE_METHOD(tpl, "setSessionID",    SetSessionID);
            NODE_SET_PROTOTYPE_METHOD(tpl, "reConnect",       ReConnect);

            constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());
            exports->Set(context,
                         String::NewFromUtf8(isolate, "ClientConnection").ToLocalChecked(),
                         tpl->GetFunction(context).ToLocalChecked()).FromJust();
        }

        authdb::client::ClientConnection& wrapped() { return _con; }

        static void NewInstance(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            Local<Context> context = isolate->GetCurrentContext();
            Local<Function> cons = Local<Function>::New(isolate, constructor);
            Local<Object> instance = cons->NewInstance(context, 0, nullptr).ToLocalChecked();
            args.GetReturnValue().Set(instance);
        }

    private:
        explicit AuthDBConnectionJS() : _con() {}
        ~AuthDBConnectionJS() override = default;

        static void New(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.IsConstructCall()) {
                AuthDBConnectionJS* obj = new AuthDBConnectionJS();
                obj->Wrap(args.This());
                args.GetReturnValue().Set(args.This());
            } else {
                Local<Context> context = isolate->GetCurrentContext();
                Local<Function> cons = Local<Function>::New(isolate, constructor);
                args.GetReturnValue().Set(cons->NewInstance(context, 0, nullptr).ToLocalChecked());
            }
        }

        /* -- setUrl(url: string) -- */
        static void SetUrl(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "setUrl requires a string argument").ToLocalChecked()));
                return;
            }
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            try {
                self->_con.setUrl(V8ToString(isolate, args[0]));
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- setClientName(name: string) -- */
        static void SetClientName(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "setClientName requires a string argument").ToLocalChecked()));
                return;
            }
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            self->_con.setClientName(V8ToString(isolate, args[0]));
        }

        /* -- setClientSecret(secret: string) -- */
        static void SetClientSecret(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "setClientSecret requires a string argument").ToLocalChecked()));
                return;
            }
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            self->_con.setClientSecret(V8ToString(isolate, args[0]));
        }

        /* -- setUsername(username: string) -- */
        static void SetUsername(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "setUsername requires a string argument").ToLocalChecked()));
                return;
            }
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            self->_con.setUsername(V8ToString(isolate, args[0]));
        }

        /* -- setUserPassword(password: string) -- */
        static void SetUserPassword(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "setUserPassword requires a string argument").ToLocalChecked()));
                return;
            }
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            self->_con.setUserPassword(V8ToString(isolate, args[0]));
        }

        /* -- getClientId() : string -- */
        static void GetClientId(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            try {
                const uuid::uuid& id = self->_con.getClientId();
                args.GetReturnValue().Set(
                    String::NewFromUtf8(isolate, id.c_str()).ToLocalChecked());
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- getSessionID() : string -- */
        static void GetSessionID(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            try {
                const uuid::uuid& sid = self->_con.getSessionID();
                args.GetReturnValue().Set(
                    String::NewFromUtf8(isolate, sid.c_str()).ToLocalChecked());
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- setSessionID(sid: string) -- */
        static void SetSessionID(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "setSessionID requires a string argument").ToLocalChecked()));
                return;
            }
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            try {
                uuid::uuid sid(V8ToString(isolate, args[0]).c_str());
                self->_con.setSessionID(sid);
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- reConnect() -- */
        static void ReConnect(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            AuthDBConnectionJS* self = ObjectWrap::Unwrap<AuthDBConnectionJS>(args.This());
            try {
                self->_con.reConnect();
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        static Persistent<Function> constructor;
        authdb::client::ClientConnection _con;
    };

    Persistent<Function> AuthDBConnectionJS::constructor;

    /*========================================================================
     *  AuthDBClientJS  –  wraps  authdb::client::Client
     *======================================================================*/
    class AuthDBClientJS : public node::ObjectWrap {
    public:
        static void Init(Local<Object> exports) {
            Isolate* isolate = exports->GetIsolate();
            Local<Context> context = isolate->GetCurrentContext();

            Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
            tpl->SetClassName(String::NewFromUtf8(isolate, "Client").ToLocalChecked());
            tpl->InstanceTemplate()->SetInternalFieldCount(1);

            NODE_SET_PROTOTYPE_METHOD(tpl, "userAuth",       UserAuth);
            NODE_SET_PROTOTYPE_METHOD(tpl, "clientAuth",     ClientAuth);
            NODE_SET_PROTOTYPE_METHOD(tpl, "addConnection",  AddConnection);
            NODE_SET_PROTOTYPE_METHOD(tpl, "remConnection",  RemConnection);
            NODE_SET_PROTOTYPE_METHOD(tpl, "editConnection", EditConnection);
            NODE_SET_PROTOTYPE_METHOD(tpl, "sessionInfo",    SessionInfo);
            NODE_SET_PROTOTYPE_METHOD(tpl, "remSession",     RemSession);
            NODE_SET_PROTOTYPE_METHOD(tpl, "gpoCheck",       GPOCheck);
            NODE_SET_PROTOTYPE_METHOD(tpl, "gpoExists",      GPOExists);
            NODE_SET_PROTOTYPE_METHOD(tpl, "gpoAdd",          GPOAdd);
            NODE_SET_PROTOTYPE_METHOD(tpl, "changeUserPw",   ChangeUserPw);

            constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());
            exports->Set(context,
                         String::NewFromUtf8(isolate, "Client").ToLocalChecked(),
                         tpl->GetFunction(context).ToLocalChecked()).FromJust();
        }

    private:
        explicit AuthDBClientJS(Isolate* isolate, Local<Object> conObj,
                               authdb::client::ClientConnection& con)
            : _client(con) {
            _conRef.Reset(isolate, conObj);
        }
        ~AuthDBClientJS() override {
            _conRef.Reset();
        }

        /*  new Client(connectionObj)  */
        static void New(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            Local<Context> context = isolate->GetCurrentContext();

            if (!args.IsConstructCall()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "Client must be called with new").ToLocalChecked()));
                return;
            }

            if (args.Length() < 1 || !args[0]->IsObject()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "Client requires a ClientConnection argument").ToLocalChecked()));
                return;
            }

            AuthDBConnectionJS* conWrap =
                ObjectWrap::Unwrap<AuthDBConnectionJS>(args[0]->ToObject(context).ToLocalChecked());
            if (!conWrap) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "argument is not a ClientConnection").ToLocalChecked()));
                return;
            }

            try {
                Local<Object> conObj = args[0]->ToObject(context).ToLocalChecked();
                AuthDBClientJS* obj = new AuthDBClientJS(isolate, conObj, conWrap->wrapped());
                obj->Wrap(args.This());
                args.GetReturnValue().Set(args.This());
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- userAuth() : boolean -- */
        static void UserAuth(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                bool ok = self->_client.UserAuth();
                args.GetReturnValue().Set(Boolean::New(isolate, ok));
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- clientAuth() : boolean -- */
        static void ClientAuth(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                bool ok = self->_client.ClientAuth();
                args.GetReturnValue().Set(Boolean::New(isolate, ok));
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- addConnection(clientName: string, clientSecret: string) : string -- */
        static void AddConnection(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 2 || !args[0]->IsString() || !args[1]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "addConnection requires (name, secret)").ToLocalChecked()));
                return;
            }
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                std::string clientDbId = self->_client.addConnection(
                    V8ToString(isolate, args[0]),
                    V8ToString(isolate, args[1]));
                args.GetReturnValue().Set(
                    String::NewFromUtf8(isolate, clientDbId.c_str()).ToLocalChecked());
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- remConnection(clientId: string) -- */
        static void RemConnection(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "remConnection requires a clientId string").ToLocalChecked()));
                return;
            }
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                uuid::uuid cid(V8ToString(isolate, args[0]).c_str());
                self->_client.remConnection(cid);
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- editConnection(clientId: string, name: string, secret: string) -- */
        static void EditConnection(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 3 || !args[0]->IsString() || !args[1]->IsString() || !args[2]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "editConnection requires (clientId, name, secret)").ToLocalChecked()));
                return;
            }
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                uuid::uuid cid(V8ToString(isolate, args[0]).c_str());
                self->_client.editConnection(cid,
                                             V8ToString(isolate, args[1]),
                                             V8ToString(isolate, args[2]));
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- sessionInfo() : { sessionId, userId, username, domainId } -- */
        static void SessionInfo(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            Local<Context> context = isolate->GetCurrentContext();

            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                authdb::SessionData sdat;
                self->_client.SessionInfo(sdat);

                Local<Object> obj = Object::New(isolate);

                uuid::uuid sid, uid, did;
                sdat.getSid(sid);
                sdat.getUid(uid);
                sdat.getDid(did);

                obj->Set(context,
                    String::NewFromUtf8(isolate, "sessionId").ToLocalChecked(),
                    String::NewFromUtf8(isolate, sid.c_str()).ToLocalChecked()).FromJust();
                obj->Set(context,
                    String::NewFromUtf8(isolate, "userId").ToLocalChecked(),
                    String::NewFromUtf8(isolate, uid.c_str()).ToLocalChecked()).FromJust();
                obj->Set(context,
                    String::NewFromUtf8(isolate, "username").ToLocalChecked(),
                    String::NewFromUtf8(isolate, sdat.getUsername()).ToLocalChecked()).FromJust();
                obj->Set(context,
                    String::NewFromUtf8(isolate, "domainId").ToLocalChecked(),
                    String::NewFromUtf8(isolate, did.c_str()).ToLocalChecked()).FromJust();

                args.GetReturnValue().Set(obj);
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- remSession() : boolean -- */
        static void RemSession(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                bool ok = self->_client.remSession();
                args.GetReturnValue().Set(Boolean::New(isolate, ok));
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- gpoCheck(gpoId: string) : boolean -- */
        static void GPOCheck(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "gpoCheck requires a gpoId string").ToLocalChecked()));
                return;
            }
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                uuid::uuid gpoid(V8ToString(isolate, args[0]).c_str());
                bool ok = self->_client.GPOcheck(gpoid);
                args.GetReturnValue().Set(Boolean::New(isolate, ok));
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- gpoExists(gpoId: string) : boolean -- */
        static void GPOExists(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 1 || !args[0]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "gpoExists requires a gpoId string").ToLocalChecked()));
                return;
            }
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                uuid::uuid gpoid(V8ToString(isolate, args[0]).c_str());
                bool ok = self->_client.GPOexits(gpoid);
                args.GetReturnValue().Set(Boolean::New(isolate, ok));
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- gpoAdd(gpoId: string, name: string, desc: string, gpoDefault: boolean, members: string[]) : string -- */
        static void GPOAdd(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            Local<Context> context = isolate->GetCurrentContext();
            if (args.Length() < 5 || !args[0]->IsString() || !args[1]->IsString()
                || !args[2]->IsString() || !args[3]->IsBoolean() || !args[4]->IsArray()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "gpoAdd requires (gpoId, name, desc, gpoDefault, members[])").ToLocalChecked()));
                return;
            }
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                uuid::uuid gpoid(V8ToString(isolate, args[0]).c_str());
                std::string name = V8ToString(isolate, args[1]);
                std::string desc = V8ToString(isolate, args[2]);
                bool gpoDefault = args[3]->BooleanValue(isolate);
                Local<v8::Array> arr = Local<v8::Array>::Cast(args[4]);
                std::vector<std::string> members;
                for (uint32_t i = 0; i < arr->Length(); ++i) {
                    Local<Value> el = arr->Get(context, i).ToLocalChecked();
                    members.push_back(V8ToString(isolate, el));
                }
                std::string gpoId = self->_client.GPOadd(gpoid, name, desc, gpoDefault, members);
                args.GetReturnValue().Set(
                    String::NewFromUtf8(isolate, gpoId.c_str()).ToLocalChecked());
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        /* -- changeUserPw(oldPw: string, newPw: string) -- */
        static void ChangeUserPw(const FunctionCallbackInfo<Value>& args) {
            Isolate* isolate = args.GetIsolate();
            if (args.Length() < 2 || !args[0]->IsString() || !args[1]->IsString()) {
                isolate->ThrowException(Exception::TypeError(
                    String::NewFromUtf8(isolate, "changeUserPw requires (oldPw, newPw)").ToLocalChecked()));
                return;
            }
            AuthDBClientJS* self = ObjectWrap::Unwrap<AuthDBClientJS>(args.This());
            try {
                self->_client.changeUserPw(V8ToString(isolate, args[0]),
                                           V8ToString(isolate, args[1]));
            } catch (const std::exception& e) {
                isolate->ThrowException(Exception::Error(
                    String::NewFromUtf8(isolate, e.what()).ToLocalChecked()));
            }
        }

        static Persistent<Function> constructor;
        Persistent<Object> _conRef;
        authdb::client::Client _client;
    };

    Persistent<Function> AuthDBClientJS::constructor;

    /*========================================================================
     *  Module initialisation
     *======================================================================*/
    void InitAll(Local<Object> exports) {
        AuthDBConnectionJS::Init(exports);
        AuthDBClientJS::Init(exports);
    }

    NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

};
