@joystick.js/db-canary 0.0.0-canary.2295 → 0.0.0-canary.2297
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/lib/operations/admin.js +1 -1
- package/dist/server/lib/user_auth_manager.js +1 -0
- package/package.json +2 -2
- package/src/client/index.js +21 -7
- package/src/server/index.js +60 -19
- package/src/server/lib/operations/admin.js +91 -2
- package/src/server/lib/user_auth_manager.js +745 -0
- package/tests/client/index.test.js +468 -69
- package/tests/server/index.test.js +210 -43
- package/tests/server/integration/authentication_integration.test.js +65 -33
- package/tests/server/integration/development_mode_authentication.test.js +21 -7
- package/tests/server/integration/production_safety_integration.test.js +24 -34
- package/tests/server/integration/replication_integration.test.js +17 -25
- package/tests/server/lib/operations/admin.test.js +39 -5
- package/tests/server/lib/user_auth_manager.test.js +525 -0
package/dist/client/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import h from"net";import{EventEmitter as l}from"events";import{encode as m,decode as p}from"msgpackr";import f from"./database.js";const _=()=>({useFloat32:!1,int64AsType:"number",mapsAsObjects:!0}),g=s=>{const e=m(s,_()),t=Buffer.allocUnsafe(4);return t.writeUInt32BE(e.length,0),Buffer.concat([t,e])},q=(s,e)=>{const t=s.slice(0,e),n=s.slice(e);try{return{message:p(t,_()),buffer:n}}catch(i){throw new Error(`Invalid message format: ${i.message}`)}},y=s=>{if(s.length<4)return{expected_length:null,buffer:s};const e=s.readUInt32BE(0),t=s.slice(4);return{expected_length:e,buffer:t}},b=()=>{let s=Buffer.alloc(0),e=null;return{parse_messages:i=>{s=Buffer.concat([s,i]);const r=[];for(;s.length>0;){if(e===null){const c=y(s);if(e=c.expected_length,s=c.buffer,e===null)break}if(s.length<e)break;const a=q(s,e);r.push(a.message),s=a.buffer,e=null}return r},reset:()=>{s=Buffer.alloc(0),e=null}}},w=(s,e)=>Math.min(e*Math.pow(2,s-1),3e4),k=(s={})=>({host:s.host||"localhost",port:s.port||1983,password:s.password||null,timeout:s.timeout||5e3,reconnect:s.reconnect!==!1,max_reconnect_attempts:s.max_reconnect_attempts||10,reconnect_delay:s.reconnect_delay||1e3,auto_connect:s.auto_connect!==!1}),x=(s,e,t)=>setTimeout(()=>{s&&!s.destroyed&&(s.destroy(),e(new Error("Connection timeout")))},t),E=(s,e,t)=>setTimeout(()=>{const n=s.get(e);n&&(s.delete(e),n.reject(new Error("Request timeout")))},t),u=(s,e)=>{for(const[t,{reject:n,timeout:i}]of s)clearTimeout(i),n(new Error(e));s.clear()},T=s=>s.ok===1||s.ok===!0,v=s=>s.ok===0||s.ok===!1,B=s=>typeof s.error=="string"?s.error:JSON.stringify(s.error)||"Operation failed";class d extends l{constructor(e={}){super();const t=k(e);this.host=t.host,this.port=t.port,this.password=t.password,this.timeout=t.timeout,this.reconnect=t.reconnect,this.max_reconnect_attempts=t.max_reconnect_attempts,this.reconnect_delay=t.reconnect_delay,this.socket=null,this.message_parser=null,this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.reconnect_attempts=0,this.reconnect_timeout=null,this.pending_requests=new Map,this.request_id_counter=0,this.request_queue=[],t.auto_connect&&this.connect()}connect(){if(this.is_connecting||this.is_connected)return;this.is_connecting=!0,this.socket=new h.Socket,this.message_parser=b();const e=x(this.socket,this.handle_connection_error.bind(this),this.timeout);this.setup_socket_handlers(e),this.socket.connect(this.port,this.host,()=>{this.handle_successful_connection(e)})}setup_socket_handlers(e){this.socket.on("data",t=>{this.handle_incoming_data(t)}),this.socket.on("error",t=>{clearTimeout(e),this.handle_connection_error(t)}),this.socket.on("close",()=>{clearTimeout(e),this.handle_disconnect()})}handle_successful_connection(e){clearTimeout(e),this.is_connected=!0,this.is_connecting=!1,this.reconnect_attempts=0,this.emit("connect"),this.password?this.authenticate():this.handle_authentication_complete()}handle_authentication_complete(){this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue()}handle_incoming_data(e){try{const t=this.message_parser.parse_messages(e);for(const n of t)this.handle_message(n)}catch(t){this.emit("error",new Error(`Message parsing failed: ${t.message}`))}}async authenticate(){if(!this.password){this.emit("error",new Error('Password required for authentication. Provide password in client options: joystickdb.client({ password: "your_password" })')),this.disconnect();return}try{if((await this.send_request("authentication",{password:this.password})).ok===1)this.handle_authentication_complete();else throw new Error("Authentication failed")}catch(e){this.emit("error",new Error(`Authentication error: ${e.message}`)),this.disconnect()}}handle_message(e){this.pending_requests.size>0?this.handle_pending_request_response(e):this.emit("response",e)}handle_pending_request_response(e){const[t,{resolve:n,reject:i,timeout:r}]=this.pending_requests.entries().next().value;if(clearTimeout(r),this.pending_requests.delete(t),T(e))n(e);else if(v(e)){const a=B(e);i(new Error(a))}else n(e)}handle_connection_error(e){this.reset_connection_state(),u(this.pending_requests,"Connection lost"),this.emit("error",e),this.should_attempt_reconnect()?this.schedule_reconnect():this.emit("disconnect")}handle_disconnect(){this.reset_connection_state(),u(this.pending_requests,"Connection closed"),this.should_attempt_reconnect()?this.schedule_reconnect():this.emit("disconnect")}reset_connection_state(){this.is_connecting=!1,this.is_connected=!1,this.is_authenticated=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.destroy(),this.socket=null),this.message_parser&&this.message_parser.reset()}should_attempt_reconnect(){return this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts}schedule_reconnect(){this.reconnect_attempts++;const e=w(this.reconnect_attempts,this.reconnect_delay);this.emit("reconnecting",{attempt:this.reconnect_attempts,delay:e}),this.reconnect_timeout=setTimeout(()=>{this.connect()},e)}send_request(e,t={},n=!0){return new Promise((i,r)=>{const a=++this.request_id_counter,o={message:{op:e,data:t},resolve:i,reject:r,request_id:a};if(this.should_queue_request(e,n)){this.request_queue.push(o);return}this.send_request_now(o)})}should_queue_request(e,t){const i=!["authentication","setup","ping"].includes(e);return(!this.is_connected||i&&!this.is_authenticated)&&t}send_request_now(e){const{message:t,resolve:n,reject:i,request_id:r}=e,a=E(this.pending_requests,r,this.timeout);this.pending_requests.set(r,{resolve:n,reject:i,timeout:a});try{const c=g(t);this.socket.write(c)}catch(c){clearTimeout(a),this.pending_requests.delete(r),i(c)}}process_request_queue(){for(;this.request_queue.length>0&&this.is_connected&&this.is_authenticated;){const e=this.request_queue.shift();this.send_request_now(e)}}disconnect(){this.reconnect=!1,this.reconnect_timeout&&(clearTimeout(this.reconnect_timeout),this.reconnect_timeout=null),this.socket&&this.socket.end()}async backup_now(){return this.send_request("admin",{admin_action:"backup_now"})}async list_backups(){return this.send_request("admin",{admin_action:"list_backups"})}async restore_backup(e){return this.send_request("admin",{admin_action:"restore_backup",backup_name:e})}async get_replication_status(){return this.send_request("admin",{admin_action:"get_replication_status"})}async add_secondary(e){return this.send_request("admin",{admin_action:"add_secondary",...e})}async remove_secondary(e){return this.send_request("admin",{admin_action:"remove_secondary",secondary_id:e})}async sync_secondaries(){return this.send_request("admin",{admin_action:"sync_secondaries"})}async get_secondary_health(){return this.send_request("admin",{admin_action:"get_secondary_health"})}async get_forwarder_status(){return this.send_request("admin",{admin_action:"get_forwarder_status"})}async ping(){return this.send_request("ping",{},!1)}async reload(){return this.send_request("reload")}async get_auto_index_stats(){return this.send_request("admin",{admin_action:"get_auto_index_stats"})}async setup(){const e=await this.send_request("setup",{},!1);return e.data&&e.data.instructions&&console.log(e.data.instructions),e}async delete_many(e,t={},n={}){return this.send_request("delete_many",{database:"default",collection:e,filter:t,options:n})}db(e){return new f(this,e)}async list_databases(){return this.send_request("admin",{admin_action:"list_databases"})}async get_stats(){return this.send_request("admin",{admin_action:"stats"})}}class j{constructor(e,t,n){this.client=e,this.database_name=t,this.collection_name=n}async insert_one(e,t={}){return this.client.send_request("insert_one",{database:this.database_name,collection:this.collection_name,document:e,options:t})}async find_one(e={},t={}){return(await this.client.send_request("find_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).document}async find(e={},t={}){return(await this.client.send_request("find",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).documents||[]}async count_documents(e={},t={}){return(await this.client.send_request("count_documents",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).count}async update_one(e,t,n={}){return this.client.send_request("update_one",{database:this.database_name,collection:this.collection_name,filter:e,update:t,options:n})}async delete_one(e,t={}){return this.client.send_request("delete_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async delete_many(e={},t={}){return this.client.send_request("delete_many",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async bulk_write(e,t={}){return this.client.send_request("bulk_write",{database:this.database_name,collection:this.collection_name,operations:e,options:t})}async create_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:t})}async upsert_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:{...t,upsert:!0}})}async drop_index(e){return this.client.send_request("drop_index",{database:this.database_name,collection:this.collection_name,field:e})}async get_indexes(){return this.client.send_request("get_indexes",{database:this.database_name,collection:this.collection_name})}}d.Collection=j;const C={client:s=>new d(s)};var P=C;export{P as default};
|
|
1
|
+
import d from"net";import{EventEmitter as l}from"events";import{encode as m,decode as p}from"msgpackr";import f from"./database.js";const _=()=>({useFloat32:!1,int64AsType:"number",mapsAsObjects:!0}),g=s=>{const e=m(s,_()),t=Buffer.allocUnsafe(4);return t.writeUInt32BE(e.length,0),Buffer.concat([t,e])},q=(s,e)=>{const t=s.slice(0,e),n=s.slice(e);try{return{message:p(t,_()),buffer:n}}catch(i){throw new Error(`Invalid message format: ${i.message}`)}},y=s=>{if(s.length<4)return{expected_length:null,buffer:s};const e=s.readUInt32BE(0),t=s.slice(4);return{expected_length:e,buffer:t}},b=()=>{let s=Buffer.alloc(0),e=null;return{parse_messages:i=>{s=Buffer.concat([s,i]);const a=[];for(;s.length>0;){if(e===null){const c=y(s);if(e=c.expected_length,s=c.buffer,e===null)break}if(s.length<e)break;const r=q(s,e);a.push(r.message),s=r.buffer,e=null}return a},reset:()=>{s=Buffer.alloc(0),e=null}}},w=(s,e)=>Math.min(e*Math.pow(2,s-1),3e4),k=(s={})=>({host:s.host||"localhost",port:s.port||1983,authentication:s.authentication||null,timeout:s.timeout||5e3,reconnect:s.reconnect!==!1,max_reconnect_attempts:s.max_reconnect_attempts||10,reconnect_delay:s.reconnect_delay||1e3,auto_connect:s.auto_connect!==!1}),x=(s,e,t)=>setTimeout(()=>{s&&!s.destroyed&&(s.destroy(),e(new Error("Connection timeout")))},t),E=(s,e,t)=>setTimeout(()=>{const n=s.get(e);n&&(s.delete(e),n.reject(new Error("Request timeout")))},t),u=(s,e)=>{for(const[t,{reject:n,timeout:i}]of s)clearTimeout(i),n(new Error(e));s.clear()},T=s=>s.ok===1||s.ok===!0,v=s=>s.ok===0||s.ok===!1,j=s=>typeof s.error=="string"?s.error:JSON.stringify(s.error)||"Operation failed";class h extends l{constructor(e={}){if(super(),e.password&&typeof e.password=="string"&&!e.authentication)throw new Error('Authentication must be provided as an object with username and password. Use: { authentication: { username: "your_username", password: "your_password" } }');const t=k(e);this.host=t.host,this.port=t.port,this.authentication=t.authentication,this.timeout=t.timeout,this.reconnect=t.reconnect,this.max_reconnect_attempts=t.max_reconnect_attempts,this.reconnect_delay=t.reconnect_delay,this.socket=null,this.message_parser=null,this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.reconnect_attempts=0,this.reconnect_timeout=null,this.pending_requests=new Map,this.request_id_counter=0,this.request_queue=[],t.auto_connect&&this.connect()}connect(){if(this.is_connecting||this.is_connected)return;this.is_connecting=!0,this.socket=new d.Socket,this.message_parser=b();const e=x(this.socket,this.handle_connection_error.bind(this),this.timeout);this.setup_socket_handlers(e),this.socket.connect(this.port,this.host,()=>{this.handle_successful_connection(e)})}setup_socket_handlers(e){this.socket.on("data",t=>{this.handle_incoming_data(t)}),this.socket.on("error",t=>{clearTimeout(e),this.handle_connection_error(t)}),this.socket.on("close",()=>{clearTimeout(e),this.handle_disconnect()})}handle_successful_connection(e){clearTimeout(e),this.is_connected=!0,this.is_connecting=!1,this.reconnect_attempts=0,this.emit("connect"),this.authentication?this.authenticate():this.handle_authentication_complete()}handle_authentication_complete(){this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue()}handle_incoming_data(e){try{const t=this.message_parser.parse_messages(e);for(const n of t)this.handle_message(n)}catch(t){this.emit("error",new Error(`Message parsing failed: ${t.message}`))}}async authenticate(){if(!this.authentication||!this.authentication.username||!this.authentication.password){this.emit("error",new Error('Authentication required. Provide authentication object in client options: joystickdb.client({ authentication: { username: "your_username", password: "your_password" } })')),this.disconnect();return}try{if((await this.send_request("authentication",{username:this.authentication.username,password:this.authentication.password})).ok===1)this.handle_authentication_complete();else throw new Error("Authentication failed")}catch(e){this.emit("error",new Error(`Authentication error: ${e.message}`)),this.disconnect()}}handle_message(e){this.pending_requests.size>0?this.handle_pending_request_response(e):this.emit("response",e)}handle_pending_request_response(e){const[t,{resolve:n,reject:i,timeout:a}]=this.pending_requests.entries().next().value;if(clearTimeout(a),this.pending_requests.delete(t),T(e))n(e);else if(v(e)){const r=j(e);i(new Error(r))}else n(e)}handle_connection_error(e){this.reset_connection_state(),u(this.pending_requests,"Connection lost"),this.emit("error",e),this.should_attempt_reconnect()?this.schedule_reconnect():this.emit("disconnect")}handle_disconnect(){this.reset_connection_state(),u(this.pending_requests,"Connection closed"),this.should_attempt_reconnect()?this.schedule_reconnect():this.emit("disconnect")}reset_connection_state(){this.is_connecting=!1,this.is_connected=!1,this.is_authenticated=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.destroy(),this.socket=null),this.message_parser&&this.message_parser.reset()}should_attempt_reconnect(){return this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts}schedule_reconnect(){this.reconnect_attempts++;const e=w(this.reconnect_attempts,this.reconnect_delay);this.emit("reconnecting",{attempt:this.reconnect_attempts,delay:e}),this.reconnect_timeout=setTimeout(()=>{this.connect()},e)}send_request(e,t={},n=!0){return new Promise((i,a)=>{const r=++this.request_id_counter,o={message:{op:e,data:t},resolve:i,reject:a,request_id:r};if(this.should_queue_request(e,n)){this.request_queue.push(o);return}this.send_request_now(o)})}should_queue_request(e,t){const i=!["authentication","setup","ping"].includes(e);return(!this.is_connected||i&&!this.is_authenticated)&&t}send_request_now(e){const{message:t,resolve:n,reject:i,request_id:a}=e,r=E(this.pending_requests,a,this.timeout);this.pending_requests.set(a,{resolve:n,reject:i,timeout:r});try{const c=g(t);this.socket.write(c)}catch(c){clearTimeout(r),this.pending_requests.delete(a),i(c)}}process_request_queue(){for(;this.request_queue.length>0&&this.is_connected&&this.is_authenticated;){const e=this.request_queue.shift();this.send_request_now(e)}}disconnect(){this.reconnect=!1,this.reconnect_timeout&&(clearTimeout(this.reconnect_timeout),this.reconnect_timeout=null),this.socket&&this.socket.end()}async backup_now(){return this.send_request("admin",{admin_action:"backup_now"})}async list_backups(){return this.send_request("admin",{admin_action:"list_backups"})}async restore_backup(e){return this.send_request("admin",{admin_action:"restore_backup",backup_name:e})}async get_replication_status(){return this.send_request("admin",{admin_action:"get_replication_status"})}async add_secondary(e){return this.send_request("admin",{admin_action:"add_secondary",...e})}async remove_secondary(e){return this.send_request("admin",{admin_action:"remove_secondary",secondary_id:e})}async sync_secondaries(){return this.send_request("admin",{admin_action:"sync_secondaries"})}async get_secondary_health(){return this.send_request("admin",{admin_action:"get_secondary_health"})}async get_forwarder_status(){return this.send_request("admin",{admin_action:"get_forwarder_status"})}async ping(){return this.send_request("ping",{},!1)}async reload(){return this.send_request("reload")}async get_auto_index_stats(){return this.send_request("admin",{admin_action:"get_auto_index_stats"})}async setup(){const e=await this.send_request("setup",{},!1);return e.data&&e.data.instructions&&console.log(e.data.instructions),e}async delete_many(e,t={},n={}){return this.send_request("delete_many",{database:"default",collection:e,filter:t,options:n})}db(e){return new f(this,e)}async list_databases(){return this.send_request("admin",{admin_action:"list_databases"})}async get_stats(){return this.send_request("admin",{admin_action:"stats"})}async admin(e,t={}){return this.send_request("admin",{admin_action:e,...t})}}class B{constructor(e,t,n){this.client=e,this.database_name=t,this.collection_name=n}async insert_one(e,t={}){return this.client.send_request("insert_one",{database:this.database_name,collection:this.collection_name,document:e,options:t})}async find_one(e={},t={}){return(await this.client.send_request("find_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).document}async find(e={},t={}){return(await this.client.send_request("find",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).documents||[]}async count_documents(e={},t={}){return(await this.client.send_request("count_documents",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).count}async update_one(e,t,n={}){return this.client.send_request("update_one",{database:this.database_name,collection:this.collection_name,filter:e,update:t,options:n})}async delete_one(e,t={}){return this.client.send_request("delete_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async delete_many(e={},t={}){return this.client.send_request("delete_many",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async bulk_write(e,t={}){return this.client.send_request("bulk_write",{database:this.database_name,collection:this.collection_name,operations:e,options:t})}async create_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:t})}async upsert_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:{...t,upsert:!0}})}async drop_index(e){return this.client.send_request("drop_index",{database:this.database_name,collection:this.collection_name,field:e})}async get_indexes(){return this.client.send_request("get_indexes",{database:this.database_name,collection:this.collection_name})}}h.Collection=B;const A={client:s=>new h(s)};var O=A;export{O as default};
|
package/dist/server/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import k from"net";import T from"crypto";import{decode as z}from"msgpackr";import O from"./lib/op_types.js";import y from"./lib/safe_json_parse.js";import{load_settings as l,get_settings as f,get_port_configuration as _}from"./lib/load_settings.js";import{send_error as p}from"./lib/send_response.js";import{start_cluster as I}from"./cluster/index.js";import v from"./lib/logger.js";import{initialize_database as R,cleanup_database as C}from"./lib/query_engine.js";import{create_message_parser as E,encode_message as w}from"./lib/tcp_protocol.js";import{create_connection_manager as q}from"./lib/connection_manager.js";import{shutdown_write_queue as A}from"./lib/write_queue.js";import{get_client_ip as B,is_rate_limited as N,initialize_auth_manager as D,reset_auth_state as $}from"./lib/auth_manager.js";import{set_storage_engine as F,verify_credentials as J,setup_initial_admin as K,initialize_user_auth_manager as P,reset_auth_state as j,get_auth_stats as G}from"./lib/user_auth_manager.js";import{initialize_api_key_manager as M}from"./lib/api_key_manager.js";import{is_development_mode as b,display_development_startup_message as H,warn_undefined_node_env as U}from"./lib/development_mode.js";import{restore_backup as V,start_backup_schedule as W,stop_backup_schedule as Y}from"./lib/backup_manager.js";import{initialize_simple_sync_manager as L,shutdown_simple_sync_manager as Q}from"./lib/simple_sync_manager.js";import{initialize_sync_receiver as X,shutdown_sync_receiver as Z}from"./lib/sync_receiver.js";import{handle_database_operation as ee,handle_admin_operation as re,handle_ping_operation as te}from"./lib/operation_dispatcher.js";import{start_http_server as se,stop_http_server as ne}from"./lib/http_server.js";import{create_recovery_token as oe,initialize_recovery_manager as S,reset_recovery_state as ae}from"./lib/recovery_manager.js";import{has_settings as ie}from"./lib/load_settings.js";const i=new Set;let a=null;const ce=()=>T.randomBytes(16).toString("hex"),_e=e=>e&&e.username&&e.password,d=e=>({ok:0,error:e}),pe=e=>({ok:1,version:"1.0.0",message:"Authentication successful",role:e?.role||"user"}),m=(e,r)=>{const t=w(r);e.write(t),e.end()},c=(e,r)=>{const t=w(r);e.write(t)},ue=async(e,r={})=>{if(!_e(r)){const t=d("Authentication requires username and password");m(e,t);return}try{const t=B(e);if(N(t)){const n=d("Too many failed attempts. Please try again later.");m(e,n);return}const s=await J(r.username,r.password,t);if(s.success){i.add(e.id);const n=pe(s.user);c(e,n)}else{const n=d(s.error||"Authentication failed");m(e,n)}}catch{const s=d("Authentication failed");m(e,s)}},de=e=>({ok:1,password:e,message:"Authentication setup completed successfully. Save this password - it will not be shown again."}),g=e=>({ok:0,error:`Setup error: ${e}`}),me=async(e,r={})=>{try{if((await G()).configured){const u=g("Authentication already configured");c(e,u);return}const s=ce(),n=await K("admin",s,"admin@localhost.local");if(!n.success){const u=g(n.error);c(e,u);return}const o=de(s);c(e,o)}catch(t){const s=g(t.message);c(e,s)}},le=(e="")=>{if(!e)throw new Error("Must pass an op type for operation.");return O.includes(e)},fe=e=>y(e),ge=e=>{try{const r=z(e);return typeof r=="string"?y(r):r}catch{return null}},fr=e=>{try{return typeof e=="string"?fe(e):Buffer.isBuffer(e)?ge(e):e}catch{return null}},h=e=>b()?!0:i.has(e.id),he=async(e,r)=>{if(e?.restore_from)try{r.info("Startup restore requested",{backup_filename:e.restore_from});const t=await V(e.restore_from);r.info("Startup restore completed",{backup_filename:e.restore_from,duration_ms:t.duration_ms});const s={...e};delete s.restore_from,process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(s),l(),r.info("Removed restore_from from settings after successful restore")}catch(t){r.error("Startup restore failed",{backup_filename:e.restore_from,error:t.message}),r.info("Continuing with fresh database after restore failure")}},ye=()=>{try{return l(),f()}catch{return null}},ve=async e=>{const{tcp_port:r}=_(),t=e?.data_path||`./.joystick/data/joystickdb_${r}`,s=R(t);D(),P(),F(s),await M(),S()},we=e=>{try{L(),e.info("Simple sync manager initialized")}catch(r){e.warn("Failed to initialize simple sync manager",{error:r.message})}},be=e=>{X().then(()=>{e.info("Sync receiver initialized")}).catch(r=>{e.warn("Failed to initialize sync receiver",{error:r.message})})},Se=(e,r)=>{if(e?.s3)try{W(),r.info("Backup scheduling started")}catch(t){r.warn("Failed to start backup scheduling",{error:t.message})}},xe=async(e,r)=>{try{const t=await se(e);return t&&r.info("HTTP server started",{http_port:e}),t}catch(t){return r.warn("Failed to start HTTP server",{error:t.message}),null}},ke=()=>{if(b()){const{tcp_port:e,http_port:r}=_();H(e,r)}else U()},Te=()=>q({max_connections:1e3,idle_timeout:600*1e3,request_timeout:5*1e3}),ze=async(e,r,t,s)=>{a.update_activity(e.id);try{const n=t.parse_messages(r);for(const o of n)await Oe(e,o,r.length,s)}catch(n){s.error("Message parsing failed",{client_id:e.id,error:n.message}),p(e,{message:"Invalid message format"}),e.end()}},Oe=async(e,r,t,s)=>{const n=r,o=n?.op||null;if(!o){p(e,{message:"Missing operation type"});return}if(!le(o)){p(e,{message:"Invalid operation type"});return}const x=a.create_request_timeout(e.id,o);try{await Ie(e,o,n,t)}finally{clearTimeout(x)}},Ie=async(e,r,t,s)=>{const n=t?.data||{};switch(r){case"authentication":await ue(e,n);break;case"setup":await me(e,n);break;case"insert_one":case"update_one":case"delete_one":case"delete_many":case"bulk_write":case"find_one":case"find":case"count_documents":case"create_index":case"drop_index":case"get_indexes":await ee(e,r,n,h,s,a,i);break;case"ping":te(e);break;case"admin":await re(e,n,h,a,i);break;case"reload":await Re(e);break;default:p(e,{message:`Operation ${r} not implemented`})}},Re=async e=>{if(!h(e)){p(e,{message:"Authentication required"});return}try{const r=Ce(),t=await Ee(),s=qe(r,t);c(e,s)}catch(r){const t={ok:0,error:`Reload operation failed: ${r.message}`};c(e,t)}},Ce=()=>{try{return f()}catch{return null}},Ee=async()=>{try{return await l(),f()}catch{return{port:1983,authentication:{}}}},qe=(e,r)=>({ok:1,status:"success",message:"Configuration reloaded successfully",changes:{port_changed:e?e.port!==r.port:!1,authentication_changed:e?e.authentication?.password_hash!==r.authentication?.password_hash:!1},timestamp:new Date().toISOString()}),Ae=(e,r)=>{r.info("Client disconnected",{socket_id:e.id}),i.delete(e.id),a.remove_connection(e.id)},Be=(e,r,t)=>{t.error("Socket error",{socket_id:e.id,error:r.message}),i.delete(e.id),a.remove_connection(e.id)},Ne=(e,r,t)=>{e.on("data",async s=>{await ze(e,s,r,t)}),e.on("end",()=>{Ae(e,t)}),e.on("error",s=>{Be(e,s,t)})},De=(e,r)=>{if(!a.add_connection(e))return;const t=E();Ne(e,t,r)},$e=()=>async()=>{try{await ne(),Y(),await Q(),await Z(),a&&a.shutdown(),i.clear(),await A(),await new Promise(e=>setTimeout(e,100)),await C(),$(),j(),ae()}catch{}},gr=async()=>{const{create_context_logger:e}=v("server"),r=e(),t=ye();await he(t,r),await ve(t),we(r),be(r),Se(t,r),a=Te();const{http_port:s}=_();await xe(s,r),ke();const n=k.createServer((o={})=>{De(o,r)});return n.cleanup=$e(),n},Fe=e=>{try{S();const r=oe();console.log("Emergency Recovery Token Generated"),console.log(`Visit: ${r.url}`),console.log("Token expires in 10 minutes"),e.info("Recovery token generated via CLI",{expires_at:new Date(r.expires_at).toISOString()}),process.exit(0)}catch(r){console.error("Failed to generate recovery token:",r.message),e.error("Recovery token generation failed",{error:r.message}),process.exit(1)}},Je=()=>{const{tcp_port:e}=_();return{worker_count:process.env.WORKER_COUNT?parseInt(process.env.WORKER_COUNT):void 0,port:e,environment:process.env.NODE_ENV||"development"}},Ke=(e,r)=>{const{tcp_port:t,http_port:s}=_(),n=ie();r.info("Starting JoystickDB server...",{workers:e.worker_count||"auto",tcp_port:t,http_port:s,environment:e.environment,has_settings:n,port_source:n?"JOYSTICK_DB_SETTINGS":"default"})};if(import.meta.url===`file://${process.argv[1]}`){const{create_context_logger:e}=v("main"),r=e();process.argv.includes("--generate-recovery-token")&&Fe(r);const t=Je();Ke(t,r),I(t)}export{ue as authentication,le as check_op_type,gr as create_server,fr as parse_data,me as setup};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{get_database as h}from"../query_engine.js";import{get_settings as q}from"../load_settings.js";import{get_write_queue as z}from"../write_queue.js";import{get_auth_stats as M}from"../auth_manager.js";import{get_query_statistics as P,get_auto_index_statistics as N,force_index_evaluation as R,remove_automatic_indexes as j}from"../auto_index_manager.js";import{create_index as A,drop_index as U,get_indexes as W}from"../index_manager.js";import{test_s3_connection as T,create_backup as J,list_backups as B,restore_backup as I,cleanup_old_backups as L}from"../backup_manager.js";import{get_simple_sync_manager as x}from"../simple_sync_manager.js";import{get_sync_receiver as v}from"../sync_receiver.js";import K from"../logger.js";import{performance_monitor as w}from"../performance_monitor.js";const{create_context_logger:g}=K("admin"),Y=()=>{try{return q()}catch{return{port:1983}}},G=t=>{try{const e=t.getStats?t.getStats():{};return{pageSize:e.pageSize||0,treeDepth:e.treeDepth||0,treeBranchPages:e.treeBranchPages||0,treeLeafPages:e.treeLeafPages||0,entryCount:e.entryCount||0,mapSize:e.mapSize||0,lastPageNumber:e.lastPageNumber||0}}catch{return{error:"Could not retrieve database stats"}}},H=(t,e)=>{const r={};let n=0;try{for(const{key:s}of t.getRange())if(typeof s=="string"&&s.includes(":")&&!s.startsWith("_")){const a=s.split(":")[0];r[a]=(r[a]||0)+1,n++}}catch(s){e.warn("Could not iterate database range for stats",{error:s.message})}return{collections:r,total_documents:n}},S=()=>{const t=process.memoryUsage();return{rss:Math.round(t.rss/1024/1024),heapTotal:Math.round(t.heapTotal/1024/1024),heapUsed:Math.round(t.heapUsed/1024/1024),external:Math.round(t.external/1024/1024)}},Q=t=>t.mapSize>0?Math.round(t.lastPageNumber*t.pageSize/t.mapSize*100):0,V=t=>({uptime:Math.floor(process.uptime()),uptime_formatted:F(process.uptime()),memory_usage:t,memory_usage_raw:process.memoryUsage(),node_version:process.version,platform:process.platform,arch:process.arch,pid:process.pid,cpu_usage:process.cpuUsage()}),X=(t,e,r,n)=>({total_documents:t,total_collections:Object.keys(e).length,collections:e,stats:r,map_size_usage_percent:n,disk_usage:{map_size_mb:Math.round((r.mapSize||0)/1024/1024),used_space_mb:Math.round((r.lastPageNumber||0)*(r.pageSize||0)/1024/1024)}}),Z=()=>{const t=g();try{const e=h(),r=Y(),n=G(e),{collections:s,total_documents:a}=H(e,t),o=S(),i=Q(n);return{server:V(o),database:X(a,s,n,i),performance:{ops_per_second:C(),avg_response_time_ms:D()}}}catch(e){throw t.error("Failed to get enhanced stats",{error:e.message}),e}},F=t=>{const e=Math.floor(t/86400),r=Math.floor(t%86400/3600),n=Math.floor(t%3600/60),s=Math.floor(t%60);return e>0?`${e}d ${r}h ${n}m ${s}s`:r>0?`${r}h ${n}m ${s}s`:n>0?`${n}m ${s}s`:`${s}s`};let $=0,E=0,ee=Date.now();const C=()=>{const t=(Date.now()-ee)/1e3;return t>0?Math.round($/t):0},D=()=>$>0?Math.round(E/$):0,te=t=>{$++,E+=t},se=t=>({name:t,document_count:0,indexes:[],estimated_size_bytes:0}),re=(t,e,r)=>{const n={};let s=0;try{for(const{key:a}of t.getRange())if(typeof a=="string"&&a.includes(":")&&!a.startsWith("_")){const o=a.split(":");if(o.length>=3){const i=o[0],c=o[1];i===e&&(n[c]||(n[c]=se(c)),n[c].document_count++,s++)}}}catch(a){r.warn("Could not iterate database range for collections",{error:a.message})}return{collections_map:n,total_documents:s}},oe=(t,e,r)=>{const n=["admin_test","test_collection","queue_test","users","products","orders","sessions","logs","analytics","settings","another_collection","list_test","pagination_test","get_test","query_test","admin_insert_test","admin_update_test","admin_delete_test","test_data"];let s=0;for(const a of n)try{const o=`${e}:${a}:`,i=t.getRange({start:o,end:o+"\xFF"});let c=0;for(const _ of i)c++,s++;c>0&&(r[a]={name:a,document_count:c,indexes:[],estimated_size_bytes:c*100})}catch{continue}return s},ne=(t,e,r,n)=>{try{const s=`index:${e}:`,a=t.getRange({start:s,end:s+"\xFF"});for(const{key:o,value:i}of a)if(typeof o=="string"&&o.startsWith(s)){const c=o.substring(s.length),_=c.split(":")[0],u=c.split(":")[1];r[_]&&u&&(r[_].indexes.includes(u)||r[_].indexes.push(u))}}catch(s){n.warn("Could not iterate index range",{error:s.message})}},ae=(t,e)=>{const r={},n=["default","test","admin"];for(const s of n)try{const a=O(s);(a.total_documents>0||a.total_collections>0)&&(r[s]={name:s,collections:new Set,documents_count:a.total_documents},a.collections.forEach(o=>{r[s].collections.add(o.name)}))}catch(a){e.warn("Failed to scan database in fallback",{database:s,error:a.message})}return r},ce=()=>{const t=g();try{const e=h();let r={};try{for(const{key:s}of e.getRange())if(typeof s=="string"&&s.includes(":")&&!s.startsWith("_")){const a=s.split(":");if(a.length>=3){const o=a[0],i=a[1];r[o]||(r[o]={name:o,collections:new Set,documents_count:0}),r[o].documents_count++,r[o].collections.add(i)}}}catch(s){t.warn("Could not iterate database range for databases",{error:s.message}),r=ae(e,t)}const n=Object.values(r).map(s=>({name:s.name,collections_count:s.collections.size,documents_count:s.documents_count,size_bytes:s.documents_count*100}));return r.default||n.push({name:"default",collections_count:0,documents_count:0,size_bytes:0}),{databases:n,total_databases:n.length}}catch(e){throw t.error("Failed to list databases",{error:e.message}),e}},O=(t="default")=>{const e=g();try{const r=h();let{collections_map:n,total_documents:s}=re(r,t,e);Object.keys(n).length===0&&(s+=oe(r,t,n)),ne(r,t,n,e);const a=Object.values(n);return{collections:a,total_collections:a.length,total_documents:s}}catch(r){throw e.error("Failed to list collections",{error:r.message}),r}},ie=(t,e={})=>{const r=g();if(!t)throw new Error("Collection name is required");try{const n=h(),{limit:s=50,skip:a=0,sort_field:o,sort_order:i="asc",database:c="default"}=e,_=[],u=`${c}:${t}:`;let m=0,p=0;for(const{key:d,value:y}of n.getRange({start:u,end:u+"\xFF"}))if(typeof d=="string"&&d.startsWith(u)){if(p<a){p++;continue}if(m>=s)break;try{const l=JSON.parse(y),f=d.substring(u.length);_.push({_id:f,...l}),m++}catch(l){r.warn("Could not parse document",{collection:t,key:d,error:l.message})}}return o&&_.length>0&&_.sort((d,y)=>{const l=d[o],f=y[o];return i==="desc"?f>l?1:f<l?-1:0:l>f?1:l<f?-1:0}),{collection:t,documents:_,count:_.length,skip:a,limit:s,has_more:m===s}}catch(n){throw r.error("Failed to list documents",{collection:t,error:n.message}),n}},_e=(t,e,r="default")=>{const n=g();if(!t||!e)throw new Error("Collection name and document ID are required");try{const s=h(),a=`${r}:${t}:${e}`,o=s.get(a);if(!o)return{found:!1,collection:t,document_id:e};const i=JSON.parse(o);return{found:!0,collection:t,document_id:e,document:{_id:e,...i}}}catch(s){throw n.error("Failed to get document",{collection:t,document_id:e,error:s.message}),s}},ue=(t,e,r,n)=>{switch(t){case"$gt":return r>e;case"$gte":return r>=e;case"$lt":return r<e;case"$lte":return r<=e;case"$ne":return r!==e;case"$in":return Array.isArray(e)&&e.includes(r);case"$regex":const s=n.$options||"";return new RegExp(e,s).test(String(r));default:return r===n}},le=(t,e)=>Object.keys(e).every(r=>{const n=e[r],s=t[r];return typeof n=="object"&&n!==null?Object.keys(n).every(a=>{const o=n[a];return ue(a,o,s,n)}):s===n}),de=(t,e,r,n,s)=>{try{const a=JSON.parse(e),i={_id:t.substring(r.length),...a};return le(i,n)?i:null}catch(a){return s.warn("Could not parse document during query",{key:t,error:a.message}),null}},me=(t,e={},r={})=>{const n=g();if(!t)throw new Error("Collection name is required");try{const s=h(),{limit:a=100,skip:o=0,database:i="default"}=r,c=[],_=`${i}:${t}:`;let u=0,m=0,p=0;for(const{key:d,value:y}of s.getRange({start:_,end:_+"\xFF"}))if(typeof d=="string"&&d.startsWith(_)){p++;const l=de(d,y,_,e,n);if(l){if(m<o){m++;continue}if(u>=a)break;c.push(l),u++}}return{collection:t,filter:e,documents:c,count:c.length,total_examined:p,skip:o,limit:a,has_more:u===a}}catch(s){throw n.error("Failed to query documents",{collection:t,filter:e,error:s.message}),s}},pe=async(t,e,r,n={})=>await(await import("./insert_one.js")).default(t,e,r,n),fe=async(t,e,r,n,s={})=>await(await import("./update_one.js")).default(t,e,r,n,s),ge=async(t,e,r,n={})=>await(await import("./delete_one.js")).default(t,e,r,n);var Se=async(t,e={},r,n)=>{const s=g(),a=Date.now();try{let o;switch(t){case"stats":const c=S();o={server:{uptime:Math.floor(process.uptime()),uptime_formatted:F(process.uptime()),node_version:process.version,platform:process.platform,arch:process.arch,pid:process.pid},memory:{heap_used_mb:c.heapUsed,heap_total_mb:c.heapTotal,rss_mb:c.rss,external_mb:c.external,heap_used_percent:c.heapTotal>0?Math.round(c.heapUsed/c.heapTotal*100):0},database:{...w.get_database_stats(),map_size_mb:Math.round((w.get_database_stats()?.map_size||0)/1024/1024),used_space_mb:Math.round((w.get_database_stats()?.used_space||0)/1024/1024),usage_percent:w.get_database_stats()?.usage_percent||0},performance:{ops_per_second:C(),avg_response_time_ms:D()},system:w.get_system_stats(),connections:r?.get_stats()||{active:n?.size||0,total:n?.size||0},write_queue:z()?.get_stats()||{},authentication:{authenticated_clients:n?.size||0,...M()},settings:(()=>{try{return{port:q().port||1983}}catch{return{port:1983}}})()};break;case"list_collections":o=O();break;case"list_documents":o=ie(e.collection,{limit:e.limit,skip:e.skip,sort_field:e.sort_field,sort_order:e.sort_order});break;case"get_document":o=_e(e.collection,e.document_id);break;case"query_documents":o=me(e.collection,e.filter,{limit:e.limit,skip:e.skip});break;case"insert_document":o=await pe(e.database||"default",e.collection,e.document,e.options);break;case"update_document":const _=e.document_id?{_id:e.document_id}:e.filter;o=await fe(e.database||"default",e.collection,_,e.update,e.options);break;case"delete_document":const u=e.document_id?{_id:e.document_id}:e.filter;o=await ge(e.database||"default",e.collection,u,e.options);break;case"test_s3_connection":o=await T();break;case"backup_now":o=await J();break;case"list_backups":o=await B();break;case"restore_backup":if(!e.backup_filename)throw new Error("backup_filename is required for restore operation");o=await I(e.backup_filename);break;case"cleanup_backups":o=await L();break;case"get_auto_index_stats":o=N();break;case"get_query_stats":o=P(e.collection);break;case"evaluate_auto_indexes":o=await R(e.collection);break;case"remove_auto_indexes":if(!e.collection)throw new Error("collection is required for remove_auto_indexes operation");o=await j(e.collection,e.field_names);break;case"create_index":if(!e.collection||!e.field)throw new Error("collection and field are required for create_index operation");o=await A(e.database||"default",e.collection,e.field,e.options);break;case"drop_index":if(!e.collection||!e.field)throw new Error("collection and field are required for drop_index operation");o=await U(e.database||"default",e.collection,e.field);break;case"get_indexes":if(!e.collection)throw new Error("collection is required for get_indexes operation");o={indexes:W(e.database||"default",e.collection)};break;case"list_databases":o=ce();break;case"get_sync_status":const m=x(),p=v();o={sync_manager:m.get_sync_status(),sync_receiver:p.get_sync_status()};break;case"update_secondary_nodes":if(!Array.isArray(e.secondary_nodes))throw new Error("secondary_nodes array is required for update_secondary_nodes operation");x().update_secondary_nodes(e.secondary_nodes),o={success:!0,message:"Secondary nodes updated successfully",secondary_nodes:e.secondary_nodes};break;case"force_sync":o=await x().force_sync();break;case"set_primary_role":if(typeof e.primary!="boolean")throw new Error("primary boolean value is required for set_primary_role operation");e.primary?(v().promote_to_primary(),o={success:!0,message:"Node promoted to primary successfully",role:"primary"}):o={success:!1,message:"Demoting primary to secondary requires server restart with updated configuration",role:"primary"};break;case"reload_sync_key":const l=v();if(!l.is_secondary)throw new Error("reload_sync_key can only be used on secondary nodes");await l.reload_api_key(),o={success:!0,message:"API_KEY reloaded successfully"};break;case"get_secondary_auth_status":const b=x().get_sync_status();o={secondary_count:b.secondary_count,auth_failures:b.stats.auth_failures,successful_syncs:b.stats.successful_syncs,failed_syncs:b.stats.failed_syncs,secondaries:b.secondaries};break;default:o={...Z(),connections:r?.get_stats()||{},write_queue:z()?.get_stats()||{},authentication:{authenticated_clients:n?.size||0,...M()},settings:(()=>{try{return{port:q().port||1983}}catch{return{port:1983}}})()}}const i=Date.now()-a;return te(i),s.info("Admin operation completed",{admin_action:t||"default",duration_ms:i,status:"success"}),o}catch(o){const i=Date.now()-a;throw s.error("Admin operation failed",{admin_action:t||"default",duration_ms:i,status:"error",error:o.message}),o}};export{Se as default,te as track_operation};
|
|
1
|
+
import{get_database as y}from"../query_engine.js";import{get_settings as v}from"../load_settings.js";import{get_write_queue as M}from"../write_queue.js";import{get_auth_stats as F}from"../auth_manager.js";import{create_user as W,get_user as T,update_user as J,reset_user_password as B,delete_user as I,list_users as L,setup_initial_admin as K,get_auth_stats as C,set_storage_engine as Y}from"../user_auth_manager.js";import{get_query_statistics as G,get_auto_index_statistics as H,force_index_evaluation as Q,remove_automatic_indexes as V}from"../auto_index_manager.js";import{create_index as X,drop_index as Z,get_indexes as ee}from"../index_manager.js";import{test_s3_connection as te,create_backup as se,list_backups as re,restore_backup as oe,cleanup_old_backups as ne}from"../backup_manager.js";import{get_simple_sync_manager as q}from"../simple_sync_manager.js";import{get_sync_receiver as z}from"../sync_receiver.js";import ae from"../logger.js";import{performance_monitor as k}from"../performance_monitor.js";const{create_context_logger:h}=ae("admin"),ce=()=>{try{return v()}catch{return{port:1983}}},ie=t=>{try{const e=t.getStats?t.getStats():{};return{pageSize:e.pageSize||0,treeDepth:e.treeDepth||0,treeBranchPages:e.treeBranchPages||0,treeLeafPages:e.treeLeafPages||0,entryCount:e.entryCount||0,mapSize:e.mapSize||0,lastPageNumber:e.lastPageNumber||0}}catch{return{error:"Could not retrieve database stats"}}},ue=(t,e)=>{const o={};let n=0;try{for(const{key:s}of t.getRange())if(typeof s=="string"&&s.includes(":")&&!s.startsWith("_")){const a=s.split(":")[0];o[a]=(o[a]||0)+1,n++}}catch(s){e.warn("Could not iterate database range for stats",{error:s.message})}return{collections:o,total_documents:n}},D=()=>{const t=process.memoryUsage();return{rss:Math.round(t.rss/1024/1024),heapTotal:Math.round(t.heapTotal/1024/1024),heapUsed:Math.round(t.heapUsed/1024/1024),external:Math.round(t.external/1024/1024)}},_e=t=>t.mapSize>0?Math.round(t.lastPageNumber*t.pageSize/t.mapSize*100):0,le=t=>({uptime:Math.floor(process.uptime()),uptime_formatted:O(process.uptime()),memory_usage:t,memory_usage_raw:process.memoryUsage(),node_version:process.version,platform:process.platform,arch:process.arch,pid:process.pid,cpu_usage:process.cpuUsage()}),de=(t,e,o,n)=>({total_documents:t,total_collections:Object.keys(e).length,collections:e,stats:o,map_size_usage_percent:n,disk_usage:{map_size_mb:Math.round((o.mapSize||0)/1024/1024),used_space_mb:Math.round((o.lastPageNumber||0)*(o.pageSize||0)/1024/1024)}}),me=()=>{const t=h();try{const e=y(),o=ce(),n=ie(e),{collections:s,total_documents:a}=ue(e,t),r=D(),i=_e(n);return{server:le(r),database:de(a,s,n,i),performance:{ops_per_second:N(),avg_response_time_ms:R()}}}catch(e){throw t.error("Failed to get enhanced stats",{error:e.message}),e}},O=t=>{const e=Math.floor(t/86400),o=Math.floor(t%86400/3600),n=Math.floor(t%3600/60),s=Math.floor(t%60);return e>0?`${e}d ${o}h ${n}m ${s}s`:o>0?`${o}h ${n}m ${s}s`:n>0?`${n}m ${s}s`:`${s}s`};let $=0,P=0,pe=Date.now();const N=()=>{const t=(Date.now()-pe)/1e3;return t>0?Math.round($/t):0},R=()=>$>0?Math.round(P/$):0,fe=t=>{$++,P+=t},ge=t=>({name:t,document_count:0,indexes:[],estimated_size_bytes:0}),ye=(t,e,o)=>{const n={};let s=0;try{for(const{key:a}of t.getRange())if(typeof a=="string"&&a.includes(":")&&!a.startsWith("_")){const r=a.split(":");if(r.length>=3){const i=r[0],c=r[1];i===e&&(n[c]||(n[c]=ge(c)),n[c].document_count++,s++)}}}catch(a){o.warn("Could not iterate database range for collections",{error:a.message})}return{collections_map:n,total_documents:s}},he=(t,e,o)=>{const n=["admin_test","test_collection","queue_test","users","products","orders","sessions","logs","analytics","settings","another_collection","list_test","pagination_test","get_test","query_test","admin_insert_test","admin_update_test","admin_delete_test","test_data"];let s=0;for(const a of n)try{const r=`${e}:${a}:`,i=t.getRange({start:r,end:r+"\xFF"});let c=0;for(const u of i)c++,s++;c>0&&(o[a]={name:a,document_count:c,indexes:[],estimated_size_bytes:c*100})}catch{continue}return s},be=(t,e,o,n)=>{try{const s=`index:${e}:`,a=t.getRange({start:s,end:s+"\xFF"});for(const{key:r,value:i}of a)if(typeof r=="string"&&r.startsWith(s)){const c=r.substring(s.length),u=c.split(":")[0],_=c.split(":")[1];o[u]&&_&&(o[u].indexes.includes(_)||o[u].indexes.push(_))}}catch(s){n.warn("Could not iterate index range",{error:s.message})}},we=(t,e)=>{const o={},n=["default","test","admin"];for(const s of n)try{const a=j(s);(a.total_documents>0||a.total_collections>0)&&(o[s]={name:s,collections:new Set,documents_count:a.total_documents},a.collections.forEach(r=>{o[s].collections.add(r.name)}))}catch(a){e.warn("Failed to scan database in fallback",{database:s,error:a.message})}return o},ke=()=>{const t=h();try{const e=y();let o={};try{for(const{key:s}of e.getRange())if(typeof s=="string"&&s.includes(":")&&!s.startsWith("_")){const a=s.split(":");if(a.length>=3){const r=a[0],i=a[1];o[r]||(o[r]={name:r,collections:new Set,documents_count:0}),o[r].documents_count++,o[r].collections.add(i)}}}catch(s){t.warn("Could not iterate database range for databases",{error:s.message}),o=we(e,t)}const n=Object.values(o).map(s=>({name:s.name,collections_count:s.collections.size,documents_count:s.documents_count,size_bytes:s.documents_count*100}));return o.default||n.push({name:"default",collections_count:0,documents_count:0,size_bytes:0}),{databases:n,total_databases:n.length}}catch(e){throw t.error("Failed to list databases",{error:e.message}),e}},j=(t="default")=>{const e=h();try{const o=y();let{collections_map:n,total_documents:s}=ye(o,t,e);Object.keys(n).length===0&&(s+=he(o,t,n)),be(o,t,n,e);const a=Object.values(n);return{collections:a,total_collections:a.length,total_documents:s}}catch(o){throw e.error("Failed to list collections",{error:o.message}),o}},xe=(t,e={})=>{const o=h();if(!t)throw new Error("Collection name is required");try{const n=y(),{limit:s=50,skip:a=0,sort_field:r,sort_order:i="asc",database:c="default"}=e,u=[],_=`${c}:${t}:`;let m=0,f=0;for(const{key:d,value:b}of n.getRange({start:_,end:_+"\xFF"}))if(typeof d=="string"&&d.startsWith(_)){if(f<a){f++;continue}if(m>=s)break;try{const l=JSON.parse(b),g=d.substring(_.length);u.push({_id:g,...l}),m++}catch(l){o.warn("Could not parse document",{collection:t,key:d,error:l.message})}}return r&&u.length>0&&u.sort((d,b)=>{const l=d[r],g=b[r];return i==="desc"?g>l?1:g<l?-1:0:l>g?1:l<g?-1:0}),{collection:t,documents:u,count:u.length,skip:a,limit:s,has_more:m===s}}catch(n){throw o.error("Failed to list documents",{collection:t,error:n.message}),n}},qe=(t,e,o="default")=>{const n=h();if(!t||!e)throw new Error("Collection name and document ID are required");try{const s=y(),a=`${o}:${t}:${e}`,r=s.get(a);if(!r)return{found:!1,collection:t,document_id:e};const i=JSON.parse(r);return{found:!0,collection:t,document_id:e,document:{_id:e,...i}}}catch(s){throw n.error("Failed to get document",{collection:t,document_id:e,error:s.message}),s}},$e=(t,e,o,n)=>{switch(t){case"$gt":return o>e;case"$gte":return o>=e;case"$lt":return o<e;case"$lte":return o<=e;case"$ne":return o!==e;case"$in":return Array.isArray(e)&&e.includes(o);case"$regex":const s=n.$options||"";return new RegExp(e,s).test(String(o));default:return o===n}},ve=(t,e)=>Object.keys(e).every(o=>{const n=e[o],s=t[o];return typeof n=="object"&&n!==null?Object.keys(n).every(a=>{const r=n[a];return $e(a,r,s,n)}):s===n}),ze=(t,e,o,n,s)=>{try{const a=JSON.parse(e),i={_id:t.substring(o.length),...a};return ve(i,n)?i:null}catch(a){return s.warn("Could not parse document during query",{key:t,error:a.message}),null}},Ee=(t,e={},o={})=>{const n=h();if(!t)throw new Error("Collection name is required");try{const s=y(),{limit:a=100,skip:r=0,database:i="default"}=o,c=[],u=`${i}:${t}:`;let _=0,m=0,f=0;for(const{key:d,value:b}of s.getRange({start:u,end:u+"\xFF"}))if(typeof d=="string"&&d.startsWith(u)){f++;const l=ze(d,b,u,e,n);if(l){if(m<r){m++;continue}if(_>=a)break;c.push(l),_++}}return{collection:t,filter:e,documents:c,count:c.length,total_examined:f,skip:r,limit:a,has_more:_===a}}catch(s){throw n.error("Failed to query documents",{collection:t,filter:e,error:s.message}),s}},Se=async(t,e,o,n={})=>await(await import("./insert_one.js")).default(t,e,o,n),Me=async(t,e,o,n,s={})=>await(await import("./update_one.js")).default(t,e,o,n,s),Fe=async(t,e,o,n={})=>await(await import("./delete_one.js")).default(t,e,o,n);var Be=async(t,e={},o,n)=>{const s=h(),a=Date.now();try{let r;switch(t){case"stats":const c=D();r={server:{uptime:Math.floor(process.uptime()),uptime_formatted:O(process.uptime()),node_version:process.version,platform:process.platform,arch:process.arch,pid:process.pid},memory:{heap_used_mb:c.heapUsed,heap_total_mb:c.heapTotal,rss_mb:c.rss,external_mb:c.external,heap_used_percent:c.heapTotal>0?Math.round(c.heapUsed/c.heapTotal*100):0},database:{...k.get_database_stats(),map_size_mb:Math.round((k.get_database_stats()?.map_size||0)/1024/1024),used_space_mb:Math.round((k.get_database_stats()?.used_space||0)/1024/1024),usage_percent:k.get_database_stats()?.usage_percent||0},performance:{ops_per_second:N(),avg_response_time_ms:R()},system:k.get_system_stats(),connections:o?.get_stats()||{active:n?.size||0,total:n?.size||0},write_queue:M()?.get_stats()||{},authentication:{authenticated_clients:n?.size||0,...F()},settings:(()=>{try{return{port:v().port||1983}}catch{return{port:1983}}})()};break;case"list_collections":r=j();break;case"list_documents":r=xe(e.collection,{limit:e.limit,skip:e.skip,sort_field:e.sort_field,sort_order:e.sort_order});break;case"get_document":r=qe(e.collection,e.document_id);break;case"query_documents":r=Ee(e.collection,e.filter,{limit:e.limit,skip:e.skip});break;case"insert_document":r=await Se(e.database||"default",e.collection,e.document,e.options);break;case"update_document":const u=e.document_id?{_id:e.document_id}:e.filter;r=await Me(e.database||"default",e.collection,u,e.update,e.options);break;case"delete_document":const _=e.document_id?{_id:e.document_id}:e.filter;r=await Fe(e.database||"default",e.collection,_,e.options);break;case"test_s3_connection":r=await te();break;case"backup_now":r=await se();break;case"list_backups":r=await re();break;case"restore_backup":if(!e.backup_filename)throw new Error("backup_filename is required for restore operation");r=await oe(e.backup_filename);break;case"cleanup_backups":r=await ne();break;case"get_auto_index_stats":r=H();break;case"get_query_stats":r=G(e.collection);break;case"evaluate_auto_indexes":r=await Q(e.collection);break;case"remove_auto_indexes":if(!e.collection)throw new Error("collection is required for remove_auto_indexes operation");r=await V(e.collection,e.field_names);break;case"create_index":if(!e.collection||!e.field)throw new Error("collection and field are required for create_index operation");r=await X(e.database||"default",e.collection,e.field,e.options);break;case"drop_index":if(!e.collection||!e.field)throw new Error("collection and field are required for drop_index operation");r=await Z(e.database||"default",e.collection,e.field);break;case"get_indexes":if(!e.collection)throw new Error("collection is required for get_indexes operation");r={indexes:ee(e.database||"default",e.collection)};break;case"list_databases":r=ke();break;case"get_sync_status":const m=q(),f=z();r={sync_manager:m.get_sync_status(),sync_receiver:f.get_sync_status()};break;case"update_secondary_nodes":if(!Array.isArray(e.secondary_nodes))throw new Error("secondary_nodes array is required for update_secondary_nodes operation");q().update_secondary_nodes(e.secondary_nodes),r={success:!0,message:"Secondary nodes updated successfully",secondary_nodes:e.secondary_nodes};break;case"force_sync":r=await q().force_sync();break;case"set_primary_role":if(typeof e.primary!="boolean")throw new Error("primary boolean value is required for set_primary_role operation");e.primary?(z().promote_to_primary(),r={success:!0,message:"Node promoted to primary successfully",role:"primary"}):r={success:!1,message:"Demoting primary to secondary requires server restart with updated configuration",role:"primary"};break;case"reload_sync_key":const l=z();if(!l.is_secondary)throw new Error("reload_sync_key can only be used on secondary nodes");await l.reload_api_key(),r={success:!0,message:"API_KEY reloaded successfully"};break;case"get_secondary_auth_status":const w=q().get_sync_status();r={secondary_count:w.secondary_count,auth_failures:w.stats.auth_failures,successful_syncs:w.stats.successful_syncs,failed_syncs:w.stats.failed_syncs,secondaries:w.secondaries};break;case"setup_initial_admin":if(!e.username||!e.password)throw new Error("username and password are required for setup_initial_admin operation");const A=y();Y(A);const p=await K(e.username,e.password,e.email);s.info("Setup initial admin result",{setup_result:p,success:p.success,ok:p.ok,error:p.error}),r={success:p.success,message:p.message,error:p.error,admin_user:p.admin_user};break;case"create_user":if(!e.username||!e.password)throw new Error("username and password are required for create_user operation");r=await W(e.username,e.password,e.email,e.role);break;case"get_user":if(!e.username)throw new Error("username is required for get_user operation");const E=await T(e.username);r=E?{found:!0,user:E}:{found:!1};break;case"update_user":if(!e.username)throw new Error("username is required for update_user operation");r=await J(e.username,e.updates||{});break;case"reset_user_password":if(!e.username||!e.new_password)throw new Error("username and new_password are required for reset_user_password operation");r=await B(e.username,e.new_password);break;case"delete_user":if(!e.username)throw new Error("username is required for delete_user operation");r=await I(e.username);break;case"list_users":const S=await L();r={users:S,total_users:S.length};break;case"get_user_auth_stats":r=await C();break;default:const U=await C();r={...me(),connections:o?.get_stats()||{},write_queue:M()?.get_stats()||{},authentication:{authenticated_clients:n?.size||0,...F(),...U},settings:(()=>{try{return{port:v().port||1983}}catch{return{port:1983}}})()}}const i=Date.now()-a;return fe(i),s.info("Admin operation completed",{admin_action:t||"default",duration_ms:i,status:"success"}),r}catch(r){const i=Date.now()-a;throw s.error("Admin operation failed",{admin_action:t||"default",duration_ms:i,status:"error",error:r.message}),r}};export{Be as default,fe as track_operation};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"fs";import p from"bcrypt";import k from"crypto";import C from"./logger.js";const{create_context_logger:F}=C("user_auth_manager"),c=F();import{load_settings as x,has_settings as L}from"./load_settings.js";const S=12,b=60*1e3,v=5,M=1e3,N=2,I="_admin",U="_users";let i=null,l=new Map,d=new Map,n=null;const $=e=>{n=e},ne=()=>k.randomBytes(16).toString("hex"),O=()=>{try{if(!L())return null;const e=x();return i=e,e.authentication&&e.authentication.failed_attempts&&(l=new Map(Object.entries(e.authentication.failed_attempts))),e.authentication&&e.authentication.rate_limits&&(d=new Map(Object.entries(e.authentication.rate_limits))),c.info("Settings data loaded successfully from environment variable"),i}catch(e){throw c.error("Failed to load settings data",{error:e.message}),new Error(`Failed to load settings data: ${e.message}`)}},h=()=>{try{i||(i={port:1983,authentication:{}}),i.authentication||(i.authentication={}),i.authentication.failed_attempts=Object.fromEntries(l),i.authentication.rate_limits=Object.fromEntries(d),process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(i),c.info("Settings data saved successfully to environment variable")}catch(e){if(c.error("Failed to save settings data",{error:e.message}),process.env.NODE_ENV!=="test")throw new Error(`Failed to save settings data: ${e.message}`)}},T=e=>process.env.NODE_ENV==="test"?!1:["127.0.0.1","::1","localhost"].includes(e),z=e=>e.remoteAddress||"127.0.0.1",A=e=>{if(T(e))return!1;const t=Date.now(),r=(l.get(e)||[]).filter(a=>t-a<b);if(r.length>=v){const a=d.get(e);if(a&&t<a.expires_at)return!0;const o=Math.min(M*Math.pow(N,Math.floor(r.length/v)),1800*1e3);return d.set(e,{expires_at:t+o,attempts:r.length}),h(),c.warn("IP rate limited",{ip:e,attempts:r.length,backoff_duration_ms:o}),!0}return!1},w=e=>{if(T(e))return;const t=Date.now(),s=l.get(e)||[];s.push(t);const r=s.filter(a=>t-a<b);l.set(e,r),c.warn("Failed authentication attempt recorded",{ip:e,total_recent_attempts:r.length}),h()},j=e=>{l.delete(e),d.delete(e),h()},g=e=>`${I}:${U}:${e}`,q=async(e,t,s="user")=>{const r=await p.hash(t,S),a=new Date().toISOString();return{username:e,password_hash:r,role:s,active:!0,created_at:a,last_login:null}},E=async(e,t,s,r="user")=>{try{if(!n)return{success:!1,error:"Storage engine not initialized"};if(!e||!t)return{success:!1,error:"Username and password are required"};if(typeof e!="string"||e.trim().length===0)return{success:!1,error:"Username is required and must be a non-empty string"};if(typeof t!="string"||t.length<6)return{success:!1,error:"Password is required and must be at least 6 characters"};if(r&&!["admin","user"].includes(r))return{success:!1,error:'Role must be either "admin" or "user"'};const a=g(e.trim().toLowerCase());if(await n.get(a))return{success:!1,error:"User already exists"};const u=await q(e.trim().toLowerCase(),t,r);await n.put(a,u),c.info("User created successfully",{username:u.username,role:u.role});const{password_hash:f,..._}=u;return{success:!0,message:"User created successfully",ok:1,user:_}}catch(a){return{success:!1,error:a.message}}},B=async e=>{try{if(!n)return{success:!1,error:"Storage engine not initialized"};if(!e)return{success:!1,error:"Username is required"};const t=g(e.trim().toLowerCase()),s=await n.get(t);if(!s)return{success:!1,error:"User not found"};const{password_hash:r,...a}=s;return{success:!0,user:a}}catch(t){return{success:!1,error:t.message}}},P=async(e,t)=>{try{if(!n)return{success:!1,error:"Storage engine not initialized"};if(!e)return{success:!1,error:"Username is required"};const s=g(e.trim().toLowerCase()),r=await n.get(s);if(!r)return{success:!1,error:`User '${e}' not found`};if(t.email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t.email))return{success:!1,error:"Invalid email format"};if(t.role&&!["admin","user"].includes(t.role))return{success:!1,error:'Role must be either "admin" or "user"'};const a=["role","active","email"],o={};for(const[f,_]of Object.entries(t))a.includes(f)&&(o[f]=_);const u={...r,...o};return await n.put(s,u),c.info("User updated successfully",{username:e,updates:o}),{success:!0,message:"User updated successfully"}}catch(s){return{success:!1,error:s.message}}},R=async(e,t)=>{try{if(!n)return{success:!1,error:"Storage engine not initialized"};if(!t||typeof t!="string"||t.length<6)return{success:!1,error:"Password must be at least 6 characters long"};const s=g(e.trim().toLowerCase()),r=await n.get(s);if(!r)return{success:!1,error:`User '${e}' not found`};const a=await p.hash(t,S),o={...r,password_hash:a};return await n.put(s,o),c.info("User password reset successfully",{username:e}),{success:!0,message:"Password reset successfully"}}catch(s){return{success:!1,error:s.message}}},K=async e=>{try{if(!n)return{success:!1,error:"Storage engine not initialized"};const t=g(e.trim().toLowerCase());return await n.get(t)?(await n.del(t),c.info("User deleted successfully",{username:e}),{success:!0,message:"User deleted successfully"}):{success:!1,error:"User not found"}}catch(t){return{success:!1,error:t.message}}},y=async()=>{try{if(!n)return{success:!1,error:"Storage engine not initialized"};const e=[],t=`${I}:${U}:`;try{for(const{key:s,value:r}of n.getRange({start:t,end:t+"\xFF"}))if(r&&typeof r=="object"){const{password_hash:a,...o}=r;e.push(o)}}catch(s){return c.error("Failed to list users",{error:s.message}),{success:!1,error:s.message}}return{success:!0,users:e.sort((s,r)=>s.username.localeCompare(r.username))}}catch(e){return{success:!1,error:e.message}}},J=async(e,t,s)=>{try{if(!n)return{success:!1,error:"Storage engine not initialized"};if(!e||!t)return{success:!1,error:"Username and password are required"};if(A(s))return{success:!1,error:"Too many failed attempts"};const r=Date.now();try{const a=g(e.trim().toLowerCase()),o=await n.get(a);if(!o)return w(s),c.warn("Authentication failed - user not found",{username:e,ip:s}),{success:!1,error:"Invalid credentials"};if(!o.active)return w(s),c.warn("Authentication failed - user inactive",{username:e,ip:s}),{success:!1,error:"Account is disabled"};const u=await p.compare(t,o.password_hash),f=Date.now()-r,_=100;if(f<_&&await new Promise(m=>setTimeout(m,_-f)),u){j(s);const m={...o,last_login:new Date().toISOString()};await n.put(a,m),c.info("Authentication successful",{username:e,ip:s});const{password_hash:X,...D}=m;return{success:!0,user:D}}else return w(s),c.warn("Authentication failed - invalid password",{username:e,ip:s}),{success:!1,error:"Invalid credentials"}}catch(a){return w(s),c.error("Authentication error",{username:e,ip:s,error:a.message}),{success:!1,error:a.message}}}catch(r){return{success:!1,error:r.message}}},G=async(e,t,s=null)=>{try{if(!n)return{success:!1,error:"Storage engine not initialized"};if(!e||!t||s!==null&&!s)return{success:!1,error:"Username, password, and email are required"};if(t.length<6)return{success:!1,error:"Password must be at least 6 characters long"};if(s&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s))return{success:!1,error:"Invalid email format"};const r=await y();if(r.success&&r.users.length>0)return{success:!1,error:"Initial admin user already exists"};const a=await E(e,t,s||`${e}@localhost`,"admin");if(!a.success)return a;const o=new Date().toISOString();return i||O(),i||(i={port:1983,cluster:!0,worker_count:2,authentication:{},backup:{enabled:!1},replication:{enabled:!1,role:"primary"},auto_indexing:{enabled:!0,threshold:100},performance:{monitoring_enabled:!0,log_slow_queries:!0,slow_query_threshold_ms:1e3},logging:{level:"info",structured:!0}}),i.authentication={user_based:!0,created_at:o,last_updated:o,failed_attempts:{},rate_limits:{}},h(),c.info("Initial admin setup completed",{admin_username:a.username,created_at:o}),{success:!0,admin_user:a,message:"Initial admin user created successfully",ok:1}}catch(r){return{success:!1,error:r.message}}},Y=async()=>{const e=!!(i&&i.authentication&&i.authentication.user_based);let t=0;if(e){const s=await y();t=s.success?s.users.length:0}return{configured:e,user_based:!0,user_count:t,failed_attempts_count:l.size,rate_limited_ips:d.size,created_at:i?.authentication?.created_at||null,last_updated:i?.authentication?.last_updated||null}},V=()=>{try{O()}catch(e){c.warn("Could not load settings data on startup",{error:e.message})}},W=()=>{i=null,l=new Map,d=new Map,n=null,process.env.JOYSTICK_DB_SETTINGS&&delete process.env.JOYSTICK_DB_SETTINGS};export{E as create_user,K as delete_user,Y as get_auth_stats,z as get_client_ip,B as get_user,V as initialize_user_auth_manager,A as is_rate_limited,y as list_users,W as reset_auth_state,R as reset_user_password,$ as set_storage_engine,G as setup_initial_admin,P as update_user,J as verify_credentials};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joystick.js/db-canary",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.0-canary.
|
|
5
|
-
"canary_version": "0.0.0-canary.
|
|
4
|
+
"version": "0.0.0-canary.2297",
|
|
5
|
+
"canary_version": "0.0.0-canary.2296",
|
|
6
6
|
"description": "JoystickDB - A minimalist database server for the Joystick framework",
|
|
7
7
|
"main": "./dist/server/index.js",
|
|
8
8
|
"scripts": {
|
package/src/client/index.js
CHANGED
|
@@ -120,7 +120,7 @@ const calculate_reconnect_delay = (attempt_number, base_delay) => {
|
|
|
120
120
|
const create_client_options = (options = {}) => ({
|
|
121
121
|
host: options.host || 'localhost',
|
|
122
122
|
port: options.port || 1983,
|
|
123
|
-
|
|
123
|
+
authentication: options.authentication || null,
|
|
124
124
|
timeout: options.timeout || 5000,
|
|
125
125
|
reconnect: options.reconnect !== false,
|
|
126
126
|
max_reconnect_attempts: options.max_reconnect_attempts || 10,
|
|
@@ -208,7 +208,10 @@ const extract_error_message = (message) => {
|
|
|
208
208
|
* @typedef {Object} ClientOptions
|
|
209
209
|
* @property {string} [host='localhost'] - Server hostname
|
|
210
210
|
* @property {number} [port=1983] - Server port
|
|
211
|
-
* @property {string} [password] - Authentication password (
|
|
211
|
+
* @property {string} [password] - Authentication password (deprecated, use authentication object)
|
|
212
|
+
* @property {Object} [authentication] - Authentication object with username and password
|
|
213
|
+
* @property {string} [authentication.username] - Username for authentication
|
|
214
|
+
* @property {string} [authentication.password] - Password for authentication
|
|
212
215
|
* @property {number} [timeout=5000] - Request timeout in milliseconds
|
|
213
216
|
* @property {boolean} [reconnect=true] - Enable automatic reconnection
|
|
214
217
|
* @property {number} [max_reconnect_attempts=10] - Maximum reconnection attempts
|
|
@@ -230,10 +233,15 @@ class JoystickDBClient extends EventEmitter {
|
|
|
230
233
|
constructor(options = {}) {
|
|
231
234
|
super();
|
|
232
235
|
|
|
236
|
+
// Reject old password-only authentication format
|
|
237
|
+
if (options.password && typeof options.password === 'string' && !options.authentication) {
|
|
238
|
+
throw new Error('Authentication must be provided as an object with username and password. Use: { authentication: { username: "your_username", password: "your_password" } }');
|
|
239
|
+
}
|
|
240
|
+
|
|
233
241
|
const client_options = create_client_options(options);
|
|
234
242
|
this.host = client_options.host;
|
|
235
243
|
this.port = client_options.port;
|
|
236
|
-
this.
|
|
244
|
+
this.authentication = client_options.authentication;
|
|
237
245
|
this.timeout = client_options.timeout;
|
|
238
246
|
this.reconnect = client_options.reconnect;
|
|
239
247
|
this.max_reconnect_attempts = client_options.max_reconnect_attempts;
|
|
@@ -309,7 +317,7 @@ class JoystickDBClient extends EventEmitter {
|
|
|
309
317
|
|
|
310
318
|
this.emit('connect');
|
|
311
319
|
|
|
312
|
-
if (this.
|
|
320
|
+
if (this.authentication) {
|
|
313
321
|
this.authenticate();
|
|
314
322
|
} else {
|
|
315
323
|
this.handle_authentication_complete();
|
|
@@ -342,15 +350,16 @@ class JoystickDBClient extends EventEmitter {
|
|
|
342
350
|
}
|
|
343
351
|
|
|
344
352
|
async authenticate() {
|
|
345
|
-
if (!this.password) {
|
|
346
|
-
this.emit('error', new Error('
|
|
353
|
+
if (!this.authentication || !this.authentication.username || !this.authentication.password) {
|
|
354
|
+
this.emit('error', new Error('Authentication required. Provide authentication object in client options: joystickdb.client({ authentication: { username: "your_username", password: "your_password" } })'));
|
|
347
355
|
this.disconnect();
|
|
348
356
|
return;
|
|
349
357
|
}
|
|
350
358
|
|
|
351
359
|
try {
|
|
352
360
|
const result = await this.send_request('authentication', {
|
|
353
|
-
|
|
361
|
+
username: this.authentication.username,
|
|
362
|
+
password: this.authentication.password
|
|
354
363
|
});
|
|
355
364
|
|
|
356
365
|
if (result.ok === 1) {
|
|
@@ -608,6 +617,11 @@ class JoystickDBClient extends EventEmitter {
|
|
|
608
617
|
async get_stats() {
|
|
609
618
|
return this.send_request('admin', { admin_action: 'stats' });
|
|
610
619
|
}
|
|
620
|
+
|
|
621
|
+
// NOTE: Generic Admin Operations.
|
|
622
|
+
async admin(action, data = {}) {
|
|
623
|
+
return this.send_request('admin', { admin_action: action, ...data });
|
|
624
|
+
}
|
|
611
625
|
}
|
|
612
626
|
|
|
613
627
|
/**
|
package/src/server/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import net from 'net';
|
|
10
|
+
import crypto from 'crypto';
|
|
10
11
|
import { decode as decode_messagepack } from 'msgpackr';
|
|
11
12
|
import op_types from './lib/op_types.js';
|
|
12
13
|
import safe_json_parse from './lib/safe_json_parse.js';
|
|
@@ -26,6 +27,14 @@ import {
|
|
|
26
27
|
initialize_auth_manager,
|
|
27
28
|
reset_auth_state
|
|
28
29
|
} from './lib/auth_manager.js';
|
|
30
|
+
import {
|
|
31
|
+
set_storage_engine,
|
|
32
|
+
verify_credentials,
|
|
33
|
+
setup_initial_admin,
|
|
34
|
+
initialize_user_auth_manager,
|
|
35
|
+
reset_auth_state as reset_user_auth_state,
|
|
36
|
+
get_auth_stats
|
|
37
|
+
} from './lib/user_auth_manager.js';
|
|
29
38
|
import {
|
|
30
39
|
initialize_api_key_manager,
|
|
31
40
|
verify_user_password,
|
|
@@ -62,12 +71,20 @@ const authenticated_clients = new Set();
|
|
|
62
71
|
let connection_manager = null;
|
|
63
72
|
|
|
64
73
|
/**
|
|
65
|
-
*
|
|
74
|
+
* Generates a cryptographically secure random password.
|
|
75
|
+
* @returns {string} 32-character hexadecimal password
|
|
76
|
+
*/
|
|
77
|
+
const generate_secure_password = () => {
|
|
78
|
+
return crypto.randomBytes(16).toString('hex');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validates authentication request data structure.
|
|
66
83
|
* @param {Object} data - Authentication data
|
|
67
|
-
* @returns {boolean} True if data is valid
|
|
84
|
+
* @returns {boolean} True if data structure is valid
|
|
68
85
|
*/
|
|
69
86
|
const validate_authentication_data = (data) => {
|
|
70
|
-
return data && data.password;
|
|
87
|
+
return data && data.username && data.password;
|
|
71
88
|
};
|
|
72
89
|
|
|
73
90
|
/**
|
|
@@ -82,12 +99,14 @@ const create_authentication_error_response = (error_message) => ({
|
|
|
82
99
|
|
|
83
100
|
/**
|
|
84
101
|
* Creates authentication success response.
|
|
102
|
+
* @param {Object} user - User object with role information
|
|
85
103
|
* @returns {Object} Success response object
|
|
86
104
|
*/
|
|
87
|
-
const create_authentication_success_response = () => ({
|
|
105
|
+
const create_authentication_success_response = (user) => ({
|
|
88
106
|
ok: 1,
|
|
89
107
|
version: "1.0.0",
|
|
90
|
-
message: 'Authentication successful'
|
|
108
|
+
message: 'Authentication successful',
|
|
109
|
+
role: user?.role || 'user'
|
|
91
110
|
});
|
|
92
111
|
|
|
93
112
|
/**
|
|
@@ -114,11 +133,11 @@ const send_response_to_socket = (socket, response) => {
|
|
|
114
133
|
/**
|
|
115
134
|
* Handles client authentication requests.
|
|
116
135
|
* @param {net.Socket} socket - Client socket connection
|
|
117
|
-
* @param {Object} data - Authentication data containing password
|
|
136
|
+
* @param {Object} data - Authentication data containing username and password
|
|
118
137
|
*/
|
|
119
138
|
export const authentication = async (socket, data = {}) => {
|
|
120
139
|
if (!validate_authentication_data(data)) {
|
|
121
|
-
const response = create_authentication_error_response('Authentication
|
|
140
|
+
const response = create_authentication_error_response('Authentication requires username and password');
|
|
122
141
|
send_response_and_close_socket(socket, response);
|
|
123
142
|
return;
|
|
124
143
|
}
|
|
@@ -132,19 +151,18 @@ export const authentication = async (socket, data = {}) => {
|
|
|
132
151
|
return;
|
|
133
152
|
}
|
|
134
153
|
|
|
135
|
-
const
|
|
154
|
+
const result = await verify_credentials(data.username, data.password, client_ip);
|
|
136
155
|
|
|
137
|
-
if (
|
|
138
|
-
|
|
156
|
+
if (result.success) {
|
|
157
|
+
authenticated_clients.add(socket.id);
|
|
158
|
+
const auth_response = create_authentication_success_response(result.user);
|
|
159
|
+
send_response_to_socket(socket, auth_response);
|
|
160
|
+
} else {
|
|
161
|
+
const response = create_authentication_error_response(result.error || 'Authentication failed');
|
|
139
162
|
send_response_and_close_socket(socket, response);
|
|
140
|
-
return;
|
|
141
163
|
}
|
|
142
|
-
|
|
143
|
-
authenticated_clients.add(socket.id);
|
|
144
|
-
const auth_response = create_authentication_success_response();
|
|
145
|
-
send_response_to_socket(socket, auth_response);
|
|
146
164
|
} catch (error) {
|
|
147
|
-
const response = create_authentication_error_response(
|
|
165
|
+
const response = create_authentication_error_response('Authentication failed');
|
|
148
166
|
send_response_and_close_socket(socket, response);
|
|
149
167
|
}
|
|
150
168
|
};
|
|
@@ -171,13 +189,30 @@ const create_setup_error_response = (error_message) => ({
|
|
|
171
189
|
});
|
|
172
190
|
|
|
173
191
|
/**
|
|
174
|
-
* Handles initial server setup,
|
|
192
|
+
* Handles initial server setup, creating initial admin user.
|
|
175
193
|
* @param {net.Socket} socket - Client socket connection
|
|
176
194
|
* @param {Object} data - Setup data (currently unused)
|
|
177
195
|
*/
|
|
178
196
|
export const setup = async (socket, data = {}) => {
|
|
179
197
|
try {
|
|
180
|
-
|
|
198
|
+
// Check if authentication already configured
|
|
199
|
+
const auth_stats = await get_auth_stats();
|
|
200
|
+
if (auth_stats.configured) {
|
|
201
|
+
const response = create_setup_error_response('Authentication already configured');
|
|
202
|
+
send_response_to_socket(socket, response);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Setup initial admin user
|
|
207
|
+
const password = generate_secure_password();
|
|
208
|
+
const setup_result = await setup_initial_admin('admin', password, 'admin@localhost.local');
|
|
209
|
+
|
|
210
|
+
if (!setup_result.success) {
|
|
211
|
+
const response = create_setup_error_response(setup_result.error);
|
|
212
|
+
send_response_to_socket(socket, response);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
181
216
|
const response = create_setup_success_response(password);
|
|
182
217
|
send_response_to_socket(socket, response);
|
|
183
218
|
} catch (error) {
|
|
@@ -319,8 +354,13 @@ const initialize_server_components = async (settings) => {
|
|
|
319
354
|
const { tcp_port } = get_port_configuration();
|
|
320
355
|
const database_path = settings?.data_path || `./.joystick/data/joystickdb_${tcp_port}`;
|
|
321
356
|
|
|
322
|
-
initialize_database(database_path);
|
|
357
|
+
const db = initialize_database(database_path);
|
|
323
358
|
initialize_auth_manager();
|
|
359
|
+
|
|
360
|
+
// NOTE: Initialize user authentication manager and set storage engine reference
|
|
361
|
+
initialize_user_auth_manager();
|
|
362
|
+
set_storage_engine(db);
|
|
363
|
+
|
|
324
364
|
await initialize_api_key_manager();
|
|
325
365
|
initialize_recovery_manager();
|
|
326
366
|
};
|
|
@@ -673,6 +713,7 @@ const create_server_cleanup_function = () => {
|
|
|
673
713
|
await cleanup_database();
|
|
674
714
|
|
|
675
715
|
reset_auth_state();
|
|
716
|
+
reset_user_auth_state();
|
|
676
717
|
reset_recovery_state();
|
|
677
718
|
} catch (error) {
|
|
678
719
|
// NOTE: Ignore cleanup errors in tests.
|
|
@@ -10,6 +10,17 @@ import { get_database } from '../query_engine.js';
|
|
|
10
10
|
import { get_settings } from '../load_settings.js';
|
|
11
11
|
import { get_write_queue } from '../write_queue.js';
|
|
12
12
|
import { get_auth_stats } from '../auth_manager.js';
|
|
13
|
+
import {
|
|
14
|
+
create_user,
|
|
15
|
+
get_user,
|
|
16
|
+
update_user,
|
|
17
|
+
reset_user_password,
|
|
18
|
+
delete_user,
|
|
19
|
+
list_users,
|
|
20
|
+
setup_initial_admin,
|
|
21
|
+
get_auth_stats as get_user_auth_stats,
|
|
22
|
+
set_storage_engine
|
|
23
|
+
} from '../user_auth_manager.js';
|
|
13
24
|
import {
|
|
14
25
|
get_query_statistics,
|
|
15
26
|
get_auto_index_statistics,
|
|
@@ -1051,15 +1062,93 @@ export default async (admin_action, data = {}, connection_manager, authenticated
|
|
|
1051
1062
|
};
|
|
1052
1063
|
break;
|
|
1053
1064
|
|
|
1054
|
-
|
|
1065
|
+
// NOTE: User Management Operations
|
|
1066
|
+
case 'setup_initial_admin':
|
|
1067
|
+
if (!data.username || !data.password) {
|
|
1068
|
+
throw new Error('username and password are required for setup_initial_admin operation');
|
|
1069
|
+
}
|
|
1070
|
+
// Ensure storage engine is set before calling setup_initial_admin
|
|
1071
|
+
const db = get_database();
|
|
1072
|
+
set_storage_engine(db);
|
|
1073
|
+
|
|
1074
|
+
const setup_result = await setup_initial_admin(data.username, data.password, data.email);
|
|
1075
|
+
|
|
1076
|
+
// Log the actual result for debugging
|
|
1077
|
+
log.info('Setup initial admin result', {
|
|
1078
|
+
setup_result,
|
|
1079
|
+
success: setup_result.success,
|
|
1080
|
+
ok: setup_result.ok,
|
|
1081
|
+
error: setup_result.error
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// Return the result - let dispatcher handle success/failure
|
|
1085
|
+
result = {
|
|
1086
|
+
success: setup_result.success,
|
|
1087
|
+
message: setup_result.message,
|
|
1088
|
+
error: setup_result.error,
|
|
1089
|
+
admin_user: setup_result.admin_user
|
|
1090
|
+
};
|
|
1091
|
+
break;
|
|
1092
|
+
|
|
1093
|
+
case 'create_user':
|
|
1094
|
+
if (!data.username || !data.password) {
|
|
1095
|
+
throw new Error('username and password are required for create_user operation');
|
|
1096
|
+
}
|
|
1097
|
+
result = await create_user(data.username, data.password, data.email, data.role);
|
|
1098
|
+
break;
|
|
1099
|
+
|
|
1100
|
+
case 'get_user':
|
|
1101
|
+
if (!data.username) {
|
|
1102
|
+
throw new Error('username is required for get_user operation');
|
|
1103
|
+
}
|
|
1104
|
+
const user = await get_user(data.username);
|
|
1105
|
+
result = user ? { found: true, user } : { found: false };
|
|
1106
|
+
break;
|
|
1107
|
+
|
|
1108
|
+
case 'update_user':
|
|
1109
|
+
if (!data.username) {
|
|
1110
|
+
throw new Error('username is required for update_user operation');
|
|
1111
|
+
}
|
|
1112
|
+
result = await update_user(data.username, data.updates || {});
|
|
1113
|
+
break;
|
|
1114
|
+
|
|
1115
|
+
case 'reset_user_password':
|
|
1116
|
+
if (!data.username || !data.new_password) {
|
|
1117
|
+
throw new Error('username and new_password are required for reset_user_password operation');
|
|
1118
|
+
}
|
|
1119
|
+
result = await reset_user_password(data.username, data.new_password);
|
|
1120
|
+
break;
|
|
1121
|
+
|
|
1122
|
+
case 'delete_user':
|
|
1123
|
+
if (!data.username) {
|
|
1124
|
+
throw new Error('username is required for delete_user operation');
|
|
1125
|
+
}
|
|
1126
|
+
result = await delete_user(data.username);
|
|
1127
|
+
break;
|
|
1128
|
+
|
|
1129
|
+
case 'list_users':
|
|
1130
|
+
const users = await list_users();
|
|
1131
|
+
result = {
|
|
1132
|
+
users,
|
|
1133
|
+
total_users: users.length
|
|
1134
|
+
};
|
|
1135
|
+
break;
|
|
1136
|
+
|
|
1137
|
+
case 'get_user_auth_stats':
|
|
1138
|
+
result = await get_user_auth_stats();
|
|
1139
|
+
break;
|
|
1140
|
+
|
|
1141
|
+
default:
|
|
1055
1142
|
// Default admin action (backward compatibility)
|
|
1143
|
+
const user_auth_stats = await get_user_auth_stats();
|
|
1056
1144
|
result = {
|
|
1057
1145
|
...get_enhanced_stats(),
|
|
1058
1146
|
connections: connection_manager?.get_stats() || {},
|
|
1059
1147
|
write_queue: get_write_queue()?.get_stats() || {},
|
|
1060
1148
|
authentication: {
|
|
1061
1149
|
authenticated_clients: authenticated_clients?.size || 0,
|
|
1062
|
-
...get_auth_stats()
|
|
1150
|
+
...get_auth_stats(),
|
|
1151
|
+
...user_auth_stats
|
|
1063
1152
|
},
|
|
1064
1153
|
settings: (() => {
|
|
1065
1154
|
try {
|