mirror of
https://github.com/Finsys/dockhand.git
synced 2026-06-17 19:09:33 +03:00
missing scripts
This commit is contained in:
+425
@@ -0,0 +1,425 @@
|
|||||||
|
DOCKHAND PRIVACY POLICY
|
||||||
|
|
||||||
|
Last Updated: December 14, 2025
|
||||||
|
Effective Date: December 14, 2025
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. INTRODUCTION
|
||||||
|
|
||||||
|
This Privacy Policy describes how Finsys Jaroslaw Krochmalski ("Finsys," "we,"
|
||||||
|
"us," or "our") handles data in connection with the Dockhand software
|
||||||
|
application ("Software"). This Policy applies to all users of the Software.
|
||||||
|
|
||||||
|
Finsys is committed to protecting your privacy and ensuring transparency
|
||||||
|
about our data practices. This Policy explains that the Software operates
|
||||||
|
entirely locally on your infrastructure with no data transmitted to Finsys.
|
||||||
|
|
||||||
|
|
||||||
|
2. DATA CONTROLLER INFORMATION
|
||||||
|
|
||||||
|
Finsys Jaroslaw Krochmalski
|
||||||
|
ul. Borki 6
|
||||||
|
05-119 Jozefow
|
||||||
|
Poland
|
||||||
|
|
||||||
|
VAT ID: PL7121835977
|
||||||
|
REGON: 061576391
|
||||||
|
|
||||||
|
Email: enterprise@dockhand.pro
|
||||||
|
Website: https://dockhand.pro
|
||||||
|
|
||||||
|
For the purpose of the General Data Protection Regulation (GDPR) and other
|
||||||
|
applicable data protection laws, Finsys is NOT the data controller for any
|
||||||
|
personal data processed through your installation of the Software. You (the
|
||||||
|
user or your organization) are the data controller for all data stored in
|
||||||
|
your Software installation.
|
||||||
|
|
||||||
|
|
||||||
|
3. OUR FUNDAMENTAL PRINCIPLE: LOCAL-ONLY DATA
|
||||||
|
|
||||||
|
The Software is designed with privacy as a core principle:
|
||||||
|
|
||||||
|
- ALL DATA STAYS LOCAL: The Software stores all data exclusively on your
|
||||||
|
infrastructure (your servers, your databases, your storage).
|
||||||
|
|
||||||
|
- NO DATA TRANSMISSION: The Software does not transmit any data to Finsys
|
||||||
|
servers, third-party servers, or any external services.
|
||||||
|
|
||||||
|
- NO TELEMETRY: The Software contains no telemetry, analytics, usage
|
||||||
|
tracking, crash reporting, or any other data collection mechanisms.
|
||||||
|
|
||||||
|
- FULLY SELF-CONTAINED: The Software operates entirely within your
|
||||||
|
infrastructure without requiring any connection to Finsys systems.
|
||||||
|
|
||||||
|
- FINSYS HAS NO ACCESS: Finsys cannot access, view, retrieve, or process
|
||||||
|
any data stored in your Software installation.
|
||||||
|
|
||||||
|
|
||||||
|
4. DATA PROCESSED BY THE SOFTWARE
|
||||||
|
|
||||||
|
When you use the Software, the following types of data may be stored
|
||||||
|
LOCALLY on your infrastructure:
|
||||||
|
|
||||||
|
4.1 User Account Data
|
||||||
|
- Usernames and email addresses
|
||||||
|
- Password hashes (never stored in plain text)
|
||||||
|
- Multi-factor authentication (MFA) secrets (Enterprise Edition)
|
||||||
|
- User profile information and avatars
|
||||||
|
- Role assignments and permissions (Enterprise Edition)
|
||||||
|
|
||||||
|
4.2 Authentication Data
|
||||||
|
- Session tokens and cookies
|
||||||
|
- OIDC/SSO tokens and provider configurations
|
||||||
|
- LDAP/Active Directory connection settings (Enterprise Edition)
|
||||||
|
- API tokens for remote access
|
||||||
|
|
||||||
|
4.3 Docker Environment Data
|
||||||
|
- Docker host connection details (URLs, ports, socket paths)
|
||||||
|
- Docker container information (names, IDs, configurations)
|
||||||
|
- Container logs and metrics
|
||||||
|
- Image and volume data
|
||||||
|
- Network configurations
|
||||||
|
- Compose stack definitions
|
||||||
|
|
||||||
|
4.4 Git Integration Data
|
||||||
|
- Git repository URLs and credentials
|
||||||
|
- SSH keys and access tokens
|
||||||
|
- Deployment webhooks
|
||||||
|
|
||||||
|
4.5 Registry Data
|
||||||
|
- Docker registry URLs and credentials
|
||||||
|
- Image pull/push history
|
||||||
|
|
||||||
|
4.6 Activity and Audit Data
|
||||||
|
- User activity logs
|
||||||
|
- Container events and operations
|
||||||
|
- Audit trails (Enterprise Edition)
|
||||||
|
|
||||||
|
4.7 Application Settings
|
||||||
|
- General configuration preferences
|
||||||
|
- Notification channel settings (SMTP, webhooks)
|
||||||
|
- Scheduled task configurations
|
||||||
|
|
||||||
|
All of the above data is stored exclusively in your local database
|
||||||
|
(SQLite or PostgreSQL) and on your local filesystem. None of this data
|
||||||
|
is transmitted to or accessible by Finsys.
|
||||||
|
|
||||||
|
|
||||||
|
5. HOW DATA IS STORED
|
||||||
|
|
||||||
|
5.1 Database Storage
|
||||||
|
|
||||||
|
The Software uses either SQLite or PostgreSQL as configured by you:
|
||||||
|
- SQLite: Data stored in a local file on your server
|
||||||
|
- PostgreSQL: Data stored in your PostgreSQL database instance
|
||||||
|
|
||||||
|
5.2 File Storage
|
||||||
|
|
||||||
|
Certain data is stored in the local filesystem:
|
||||||
|
- Compose stack files
|
||||||
|
- Uploaded files (e.g., user avatars)
|
||||||
|
- Temporary files during operations
|
||||||
|
|
||||||
|
5.3 Encryption
|
||||||
|
|
||||||
|
- Passwords are hashed using secure algorithms (Argon2id)
|
||||||
|
- Sensitive credentials may be encrypted at rest depending on your
|
||||||
|
database configuration
|
||||||
|
- You are responsible for implementing disk encryption, database
|
||||||
|
encryption, and network security for your infrastructure
|
||||||
|
|
||||||
|
|
||||||
|
6. YOUR RESPONSIBILITIES AS DATA CONTROLLER
|
||||||
|
|
||||||
|
Since all data is stored locally on your infrastructure, YOU are the
|
||||||
|
data controller for purposes of GDPR and other data protection laws.
|
||||||
|
As data controller, you are responsible for:
|
||||||
|
|
||||||
|
6.1 Legal Basis for Processing
|
||||||
|
Ensuring you have a valid legal basis for processing personal data of
|
||||||
|
your users (e.g., consent, legitimate interest, contractual necessity).
|
||||||
|
|
||||||
|
6.2 Data Subject Rights
|
||||||
|
Responding to data subject requests including:
|
||||||
|
- Right of access (Article 15 GDPR)
|
||||||
|
- Right to rectification (Article 16 GDPR)
|
||||||
|
- Right to erasure (Article 17 GDPR)
|
||||||
|
- Right to restriction of processing (Article 18 GDPR)
|
||||||
|
- Right to data portability (Article 20 GDPR)
|
||||||
|
- Right to object (Article 21 GDPR)
|
||||||
|
|
||||||
|
6.3 Security Measures
|
||||||
|
Implementing appropriate technical and organizational measures to
|
||||||
|
protect personal data, including:
|
||||||
|
- Access controls and authentication
|
||||||
|
- Encryption of data at rest and in transit
|
||||||
|
- Regular security updates and patches
|
||||||
|
- Backup and disaster recovery procedures
|
||||||
|
- Network security (firewalls, VPNs, etc.)
|
||||||
|
|
||||||
|
6.4 Data Retention
|
||||||
|
Establishing and implementing appropriate data retention policies.
|
||||||
|
|
||||||
|
6.5 Breach Notification
|
||||||
|
Notifying supervisory authorities and affected individuals in case
|
||||||
|
of a personal data breach, as required by applicable law.
|
||||||
|
|
||||||
|
6.6 Privacy Notices
|
||||||
|
Providing appropriate privacy notices to your users regarding how
|
||||||
|
their data is processed within the Software.
|
||||||
|
|
||||||
|
|
||||||
|
7. DATA WE DO NOT COLLECT
|
||||||
|
|
||||||
|
To be absolutely clear, Finsys does NOT collect, receive, access, or
|
||||||
|
process ANY of the following:
|
||||||
|
|
||||||
|
- Your identity or contact information (unless you contact us directly)
|
||||||
|
- Your Docker infrastructure information
|
||||||
|
- Your container configurations or data
|
||||||
|
- Your user accounts or credentials
|
||||||
|
- Your activity logs or audit trails
|
||||||
|
- Your git repositories or deployment data
|
||||||
|
- Usage statistics or analytics
|
||||||
|
- Error reports or crash data
|
||||||
|
- Any telemetry or diagnostic data
|
||||||
|
- Any data whatsoever from your Software installation
|
||||||
|
|
||||||
|
|
||||||
|
8. WHEN FINSYS MAY RECEIVE DATA
|
||||||
|
|
||||||
|
The only circumstances in which Finsys may receive data from you are:
|
||||||
|
|
||||||
|
8.1 Direct Communication
|
||||||
|
When you voluntarily contact us via email (enterprise@dockhand.pro),
|
||||||
|
we receive and process the information you provide (name, email address,
|
||||||
|
message content). This data is processed for the purpose of responding
|
||||||
|
to your inquiry based on our legitimate interest in providing customer
|
||||||
|
support.
|
||||||
|
|
||||||
|
8.2 License Purchase
|
||||||
|
|
||||||
|
When you purchase an Enterprise Edition license, we collect and process:
|
||||||
|
|
||||||
|
Data Collected:
|
||||||
|
- Name and/or company name
|
||||||
|
- Email address
|
||||||
|
- Billing address
|
||||||
|
- Payment information (processed by payment provider)
|
||||||
|
- Licensed hostname/identifier
|
||||||
|
|
||||||
|
Legal Basis (GDPR Article 6):
|
||||||
|
- Contract performance (Art. 6(1)(b)) - to fulfill the license agreement
|
||||||
|
- Legal obligation (Art. 6(1)(c)) - for invoicing and tax records
|
||||||
|
|
||||||
|
How We Use This Data:
|
||||||
|
- To issue and deliver your License Key
|
||||||
|
- To send license renewal reminders
|
||||||
|
- To provide support related to your license
|
||||||
|
- To comply with tax and accounting obligations
|
||||||
|
|
||||||
|
Data Retention:
|
||||||
|
- License and invoice records: 7 years (Polish tax law requirement)
|
||||||
|
- Email correspondence: 3 years after last contact
|
||||||
|
|
||||||
|
Data Sharing:
|
||||||
|
- Payment processor (for payment transactions only)
|
||||||
|
- No other third parties
|
||||||
|
- No marketing or advertising use
|
||||||
|
|
||||||
|
8.3 Website Visits
|
||||||
|
If you visit our website (https://dockhand.pro), standard web server
|
||||||
|
logs may be collected. See our website privacy policy for details.
|
||||||
|
|
||||||
|
|
||||||
|
9. LICENSE KEY DATA
|
||||||
|
|
||||||
|
Enterprise Edition License Keys contain:
|
||||||
|
- Customer name (as registered)
|
||||||
|
- Licensed hostname or identifier
|
||||||
|
- Expiration date
|
||||||
|
- Cryptographic signature
|
||||||
|
|
||||||
|
This information is embedded in the License Key itself and stored
|
||||||
|
locally in your Software installation. Finsys retains a record of
|
||||||
|
issued licenses for license management purposes.
|
||||||
|
|
||||||
|
|
||||||
|
10. INTERNATIONAL DATA TRANSFERS
|
||||||
|
|
||||||
|
Since all Software data is stored locally on your infrastructure, no
|
||||||
|
international data transfers occur through the Software itself.
|
||||||
|
|
||||||
|
If your infrastructure is located outside the European Economic Area
|
||||||
|
(EEA), you are responsible for ensuring appropriate safeguards for
|
||||||
|
any personal data stored therein.
|
||||||
|
|
||||||
|
|
||||||
|
11. DATA RETENTION
|
||||||
|
|
||||||
|
11.1 Software Data
|
||||||
|
You control the retention of all data in your Software installation.
|
||||||
|
The Software does not automatically delete data unless you configure
|
||||||
|
retention policies or manually delete data.
|
||||||
|
|
||||||
|
11.2 Communication Data
|
||||||
|
If you contact us directly, we retain correspondence for as long as
|
||||||
|
necessary to respond to your inquiry and for our records, typically
|
||||||
|
not exceeding 3 years unless required for legal purposes.
|
||||||
|
|
||||||
|
11.3 License Records
|
||||||
|
We retain license purchase and activation records for the duration
|
||||||
|
required by tax and accounting regulations (typically 5-7 years).
|
||||||
|
|
||||||
|
|
||||||
|
12. CHILDREN'S PRIVACY
|
||||||
|
|
||||||
|
The Software is not intended for use by children under 16 years of age.
|
||||||
|
We do not knowingly collect personal data from children. If you are a
|
||||||
|
parent or guardian and believe your child has provided personal data
|
||||||
|
to us through direct communication, please contact us.
|
||||||
|
|
||||||
|
|
||||||
|
13. THIRD-PARTY SERVICES
|
||||||
|
|
||||||
|
13.1 Software Integrations
|
||||||
|
|
||||||
|
The Software may connect to third-party services as configured by you:
|
||||||
|
- Docker registries
|
||||||
|
- Git repositories (GitHub, GitLab, etc.)
|
||||||
|
- OIDC/SSO providers
|
||||||
|
- LDAP/Active Directory servers
|
||||||
|
- Notification services (SMTP, Discord, Slack, etc.)
|
||||||
|
|
||||||
|
These connections are initiated by you, configured by you, and occur
|
||||||
|
between your infrastructure and these third-party services. Finsys is
|
||||||
|
not involved in these connections and has no access to the data
|
||||||
|
exchanged. The privacy policies of these third-party services apply
|
||||||
|
to your use of them.
|
||||||
|
|
||||||
|
13.2 No Hidden Third-Party Data Sharing
|
||||||
|
|
||||||
|
The Software does not share any data with third parties on our behalf.
|
||||||
|
There are no embedded analytics services, advertising networks, or
|
||||||
|
data brokers within the Software.
|
||||||
|
|
||||||
|
|
||||||
|
14. SECURITY
|
||||||
|
|
||||||
|
14.1 Software Security
|
||||||
|
|
||||||
|
We implement security measures in the Software design:
|
||||||
|
- Secure password hashing (Argon2id)
|
||||||
|
- Session management with secure tokens
|
||||||
|
- Input validation and sanitization
|
||||||
|
- Protection against common web vulnerabilities
|
||||||
|
|
||||||
|
14.2 Your Security Responsibilities
|
||||||
|
|
||||||
|
Since all data is stored on your infrastructure, you are responsible
|
||||||
|
for:
|
||||||
|
- Keeping the Software updated
|
||||||
|
- Securing your server and database
|
||||||
|
- Implementing network security measures
|
||||||
|
- Managing user access and authentication
|
||||||
|
- Creating and securing backups
|
||||||
|
|
||||||
|
|
||||||
|
15. CHANGES TO THIS PRIVACY POLICY
|
||||||
|
|
||||||
|
We may update this Privacy Policy from time to time. Material changes
|
||||||
|
will be communicated through:
|
||||||
|
- Updated "Last Updated" date at the top of this Policy
|
||||||
|
- Notice on our website
|
||||||
|
- Notice within the Software (for significant changes)
|
||||||
|
|
||||||
|
We encourage you to review this Privacy Policy periodically.
|
||||||
|
|
||||||
|
|
||||||
|
16. GDPR COMPLIANCE
|
||||||
|
|
||||||
|
Finsys complies with the General Data Protection Regulation (EU) 2016/679.
|
||||||
|
|
||||||
|
Summary of Our Data Processing:
|
||||||
|
- We only collect personal data (email, name) when you purchase a license
|
||||||
|
- Legal basis: Contract performance and legal obligation
|
||||||
|
- Data is stored securely in the EU (Poland)
|
||||||
|
- Retention: 7 years for tax records, 3 years for correspondence
|
||||||
|
- No automated decision-making or profiling
|
||||||
|
- No data sold or shared for marketing purposes
|
||||||
|
|
||||||
|
Your GDPR Rights (Articles 15-22):
|
||||||
|
You have the right to access, rectify, erase, restrict processing,
|
||||||
|
data portability, and object to processing of your personal data.
|
||||||
|
|
||||||
|
To exercise any of these rights, contact: enterprise@dockhand.pro
|
||||||
|
We will respond within 30 days as required by GDPR.
|
||||||
|
|
||||||
|
|
||||||
|
17. YOUR RIGHTS
|
||||||
|
|
||||||
|
If you are located in the European Economic Area (EEA), United Kingdom,
|
||||||
|
or other jurisdiction with data protection laws, you have rights
|
||||||
|
regarding personal data we hold about you (from direct communications
|
||||||
|
or license purchases):
|
||||||
|
|
||||||
|
- Access: Request access to personal data we hold about you
|
||||||
|
- Rectification: Request correction of inaccurate data
|
||||||
|
- Erasure: Request deletion of your data
|
||||||
|
- Restriction: Request restriction of processing
|
||||||
|
- Portability: Request a copy of your data in portable format
|
||||||
|
- Objection: Object to processing based on legitimate interests
|
||||||
|
- Complaint: Lodge a complaint with a supervisory authority
|
||||||
|
|
||||||
|
To exercise these rights, contact us at enterprise@dockhand.pro.
|
||||||
|
|
||||||
|
Note: These rights apply to data WE hold (from direct communication or
|
||||||
|
license purchases), not to data in YOUR Software installation. For data
|
||||||
|
in your installation, YOU are the data controller and responsible for
|
||||||
|
handling such requests from your users.
|
||||||
|
|
||||||
|
|
||||||
|
18. SUPERVISORY AUTHORITY
|
||||||
|
|
||||||
|
If you are located in Poland, the relevant supervisory authority is:
|
||||||
|
|
||||||
|
Urzad Ochrony Danych Osobowych (UODO)
|
||||||
|
ul. Stawki 2
|
||||||
|
00-193 Warszawa
|
||||||
|
Poland
|
||||||
|
https://uodo.gov.pl
|
||||||
|
|
||||||
|
If you are located in another EEA country, you may contact your local
|
||||||
|
data protection authority.
|
||||||
|
|
||||||
|
|
||||||
|
19. CONTACT US
|
||||||
|
|
||||||
|
For any privacy-related questions, concerns, or requests:
|
||||||
|
|
||||||
|
Finsys Jaroslaw Krochmalski
|
||||||
|
ul. Borki 6
|
||||||
|
05-119 Jozefow
|
||||||
|
Poland
|
||||||
|
|
||||||
|
Email: enterprise@dockhand.pro
|
||||||
|
Website: https://dockhand.pro
|
||||||
|
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
SUMMARY
|
||||||
|
|
||||||
|
Dockhand is a privacy-respecting application:
|
||||||
|
- All data stays on YOUR infrastructure
|
||||||
|
- NO data is sent to Finsys servers
|
||||||
|
- NO telemetry or analytics
|
||||||
|
- YOU are the data controller for your installation
|
||||||
|
- Finsys has NO access to your data
|
||||||
|
|
||||||
|
We believe privacy is a fundamental right, and we have designed Dockhand
|
||||||
|
to respect that right by ensuring you maintain complete control over your
|
||||||
|
data at all times.
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Copyright (c) 2025-2026 Finsys Jaroslaw Krochmalski. All rights reserved.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Build subprocess scripts as standalone bundles for production.
|
||||||
|
*
|
||||||
|
* Subprocesses run via Bun.spawn and need all dependencies bundled
|
||||||
|
* since they can't access the SvelteKit build output's chunked modules.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const subprocesses = ['metrics-subprocess', 'event-subprocess'];
|
||||||
|
|
||||||
|
console.log('[build-subprocesses] Bundling subprocess scripts...');
|
||||||
|
|
||||||
|
for (const name of subprocesses) {
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: [`./src/lib/server/subprocesses/${name}.ts`],
|
||||||
|
outdir: './build/subprocesses',
|
||||||
|
target: 'bun',
|
||||||
|
minify: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`[build-subprocesses] Failed to bundle ${name}:`);
|
||||||
|
for (const log of result.logs) {
|
||||||
|
console.error(log);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[build-subprocesses] Bundled ${name}.js`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[build-subprocesses] Done');
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to backup the database
|
||||||
|
# Automatically detects database type (SQLite or PostgreSQL)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/backup-db.sh [output_dir]
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/backup-db.sh /app/data/backups
|
||||||
|
#
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# Detect database type
|
||||||
|
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||||
|
exec "$SCRIPT_DIR/postgres/backup-db.sh" "$@"
|
||||||
|
else
|
||||||
|
exec "$SCRIPT_DIR/sqlite/backup-db.sh" "$@"
|
||||||
|
fi
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to clear all user sessions
|
||||||
|
# Automatically detects database type (SQLite or PostgreSQL)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/clear-sessions.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# Detect database type
|
||||||
|
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||||
|
exec "$SCRIPT_DIR/postgres/clear-sessions.sh" "$@"
|
||||||
|
else
|
||||||
|
exec "$SCRIPT_DIR/sqlite/clear-sessions.sh" "$@"
|
||||||
|
fi
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to create an admin user
|
||||||
|
# Automatically detects database type (SQLite or PostgreSQL)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/create-admin.sh
|
||||||
|
#
|
||||||
|
# Default credentials: admin / admin123
|
||||||
|
# CHANGE THE PASSWORD IMMEDIATELY after logging in!
|
||||||
|
#
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# Detect database type
|
||||||
|
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||||
|
exec "$SCRIPT_DIR/postgres/create-admin.sh" "$@"
|
||||||
|
else
|
||||||
|
exec "$SCRIPT_DIR/sqlite/create-admin.sh" "$@"
|
||||||
|
fi
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to disable authentication
|
||||||
|
# Automatically detects database type (SQLite or PostgreSQL)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/disable-auth.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# Detect database type
|
||||||
|
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||||
|
exec "$SCRIPT_DIR/postgres/disable-auth.sh" "$@"
|
||||||
|
else
|
||||||
|
exec "$SCRIPT_DIR/sqlite/disable-auth.sh" "$@"
|
||||||
|
fi
|
||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to export all compose stacks
|
||||||
|
# Exports docker-compose.yml files from the stacks directory
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/export-stacks.sh [output_dir]
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/export-stacks.sh /tmp/stacks-backup
|
||||||
|
#
|
||||||
|
# Default output: /app/data/stacks-export
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Export Compose Stacks"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Default paths
|
||||||
|
STACKS_DIR="${DOCKHAND_STACKS:-/home/dockhand/.dockhand/stacks}"
|
||||||
|
OUTPUT_DIR="${1:-/app/data/stacks-export}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -d "$STACKS_DIR" ] && [ -d "$HOME/.dockhand/stacks" ]; then
|
||||||
|
STACKS_DIR="$HOME/.dockhand/stacks"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$STACKS_DIR" ]; then
|
||||||
|
echo "Error: Stacks directory not found at $STACKS_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count stacks
|
||||||
|
STACK_COUNT=$(find "$STACKS_DIR" -maxdepth 1 -type d ! -path "$STACKS_DIR" 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
echo "This script will export all compose stacks."
|
||||||
|
echo ""
|
||||||
|
echo "Stacks directory: $STACKS_DIR"
|
||||||
|
echo "Output directory: $OUTPUT_DIR"
|
||||||
|
echo "Stacks found: $STACK_COUNT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$STACK_COUNT" -eq "0" ]; then
|
||||||
|
echo "No stacks found to export."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
echo "Exporting stacks..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Export each stack
|
||||||
|
find "$STACKS_DIR" -maxdepth 1 -type d ! -path "$STACKS_DIR" | while read stack_dir; do
|
||||||
|
STACK_NAME=$(basename "$stack_dir")
|
||||||
|
COMPOSE_FILE="$stack_dir/docker-compose.yml"
|
||||||
|
|
||||||
|
if [ -f "$COMPOSE_FILE" ]; then
|
||||||
|
mkdir -p "$OUTPUT_DIR/$STACK_NAME"
|
||||||
|
cp "$COMPOSE_FILE" "$OUTPUT_DIR/$STACK_NAME/"
|
||||||
|
|
||||||
|
# Also copy .env file if exists
|
||||||
|
if [ -f "$stack_dir/.env" ]; then
|
||||||
|
cp "$stack_dir/.env" "$OUTPUT_DIR/$STACK_NAME/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Exported: $STACK_NAME"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Export complete!"
|
||||||
|
echo "Stacks exported to: $OUTPUT_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "To copy from Docker container to host:"
|
||||||
|
echo " docker cp dockhand:$OUTPUT_DIR ./stacks-backup"
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to list all users
|
||||||
|
# Automatically detects database type (SQLite or PostgreSQL)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/list-users.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# Detect database type
|
||||||
|
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||||
|
exec "$SCRIPT_DIR/postgres/list-users.sh" "$@"
|
||||||
|
else
|
||||||
|
exec "$SCRIPT_DIR/sqlite/list-users.sh" "$@"
|
||||||
|
fi
|
||||||
Executable
+101
@@ -0,0 +1,101 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PostgreSQL: Emergency script to backup the database
|
||||||
|
# Creates a timestamped dump of the database
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/backup-db.sh [output_dir]
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/backup-db.sh /app/data/backups
|
||||||
|
#
|
||||||
|
# Default output: /app/data
|
||||||
|
#
|
||||||
|
# Requires: DATABASE_URL environment variable
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Backup Database (PostgreSQL)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check DATABASE_URL
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "Error: DATABASE_URL environment variable not set"
|
||||||
|
echo ""
|
||||||
|
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OUTPUT_DIR="${1:-/app/data}"
|
||||||
|
|
||||||
|
# Parse DATABASE_URL
|
||||||
|
# Format: postgres://user:password@host:port/database
|
||||||
|
DB_URL="$DATABASE_URL"
|
||||||
|
DB_URL="${DB_URL#postgres://}"
|
||||||
|
DB_URL="${DB_URL#postgresql://}"
|
||||||
|
|
||||||
|
# Extract credentials
|
||||||
|
DB_USER="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PASS="${DB_URL%%@*}"
|
||||||
|
DB_URL="${DB_URL#*@}"
|
||||||
|
DB_HOST="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PORT="${DB_URL%%/*}"
|
||||||
|
DB_NAME="${DB_URL#*/}"
|
||||||
|
DB_NAME="${DB_NAME%%\?*}"
|
||||||
|
|
||||||
|
# Generate backup filename with timestamp
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
BACKUP_FILE="$OUTPUT_DIR/dockhand_backup_$TIMESTAMP.sql"
|
||||||
|
|
||||||
|
echo "This script will create a backup of the database."
|
||||||
|
echo ""
|
||||||
|
echo "Host: $DB_HOST:$DB_PORT"
|
||||||
|
echo "Database: $DB_NAME"
|
||||||
|
echo "Backup: $BACKUP_FILE"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create output directory if needed
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
echo "Creating database backup..."
|
||||||
|
|
||||||
|
# Use pg_dump to create backup
|
||||||
|
export PGPASSWORD="$DB_PASS"
|
||||||
|
if command -v pg_dump >/dev/null 2>&1; then
|
||||||
|
pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -F p -f "$BACKUP_FILE"
|
||||||
|
else
|
||||||
|
echo "Error: pg_dump not found"
|
||||||
|
echo "Install PostgreSQL client tools to use this script"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -eq 0 ] && [ -f "$BACKUP_FILE" ]; then
|
||||||
|
SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}')
|
||||||
|
echo ""
|
||||||
|
echo "Backup created successfully!"
|
||||||
|
echo "Size: $SIZE"
|
||||||
|
echo ""
|
||||||
|
echo "To copy from Docker container to host:"
|
||||||
|
echo " docker cp dockhand:$BACKUP_FILE ./dockhand_backup_$TIMESTAMP.sql"
|
||||||
|
else
|
||||||
|
echo "Error: Failed to create backup"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+75
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PostgreSQL: Emergency script to clear all user sessions
|
||||||
|
# Use this to force all users to re-login
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/clear-sessions.sh
|
||||||
|
#
|
||||||
|
# Requires: DATABASE_URL environment variable
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Clear All Sessions (PostgreSQL)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "This script will clear all user sessions,"
|
||||||
|
echo "forcing all users to log in again."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check DATABASE_URL
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "Error: DATABASE_URL environment variable not set"
|
||||||
|
echo ""
|
||||||
|
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse DATABASE_URL
|
||||||
|
DB_URL="$DATABASE_URL"
|
||||||
|
DB_URL="${DB_URL#postgres://}"
|
||||||
|
DB_URL="${DB_URL#postgresql://}"
|
||||||
|
|
||||||
|
DB_USER="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PASS="${DB_URL%%@*}"
|
||||||
|
DB_URL="${DB_URL#*@}"
|
||||||
|
DB_HOST="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PORT="${DB_URL%%/*}"
|
||||||
|
DB_NAME="${DB_URL#*/}"
|
||||||
|
DB_NAME="${DB_NAME%%\?*}"
|
||||||
|
|
||||||
|
export PGPASSWORD="$DB_PASS"
|
||||||
|
|
||||||
|
COUNT=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM sessions;" 2>/dev/null | tr -d ' ')
|
||||||
|
|
||||||
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||||
|
echo "Active sessions: $COUNT"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Clearing all user sessions..."
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "DELETE FROM sessions;"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Cleared $COUNT session(s) successfully."
|
||||||
|
echo "All users will need to log in again."
|
||||||
|
else
|
||||||
|
echo "Error: Failed to clear sessions"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+117
@@ -0,0 +1,117 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PostgreSQL: Emergency script to create an admin user
|
||||||
|
# Use this if you're locked out of Dockhand and need to create a new admin
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/create-admin.sh
|
||||||
|
#
|
||||||
|
# Default credentials: admin / admin123
|
||||||
|
# CHANGE THE PASSWORD IMMEDIATELY after logging in!
|
||||||
|
#
|
||||||
|
# Requires: DATABASE_URL environment variable
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Create Admin User (PostgreSQL)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "This script will create an admin user with:"
|
||||||
|
echo " Username: admin"
|
||||||
|
echo " Password: admin123"
|
||||||
|
echo ""
|
||||||
|
echo "If user 'admin' already exists, password will"
|
||||||
|
echo "be reset and admin privileges restored."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check DATABASE_URL
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "Error: DATABASE_URL environment variable not set"
|
||||||
|
echo ""
|
||||||
|
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse DATABASE_URL
|
||||||
|
DB_URL="$DATABASE_URL"
|
||||||
|
DB_URL="${DB_URL#postgres://}"
|
||||||
|
DB_URL="${DB_URL#postgresql://}"
|
||||||
|
|
||||||
|
DB_USER="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PASS="${DB_URL%%@*}"
|
||||||
|
DB_URL="${DB_URL#*@}"
|
||||||
|
DB_HOST="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PORT="${DB_URL%%/*}"
|
||||||
|
DB_NAME="${DB_URL#*/}"
|
||||||
|
DB_NAME="${DB_NAME%%\?*}"
|
||||||
|
|
||||||
|
export PGPASSWORD="$DB_PASS"
|
||||||
|
|
||||||
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Username and password
|
||||||
|
USERNAME="admin"
|
||||||
|
# Password: admin123
|
||||||
|
# This is an argon2id hash of "admin123" - generated with default argon2 settings
|
||||||
|
PASSWORD_HASH='$argon2id$v=19$m=65536,t=3,p=4$Jq4am2SfyYKmc0PAHe+yzg$cq/27vK/Qg2eZb/jMDy0ExLDhOG+58cKAximxpG5Dss'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Creating admin user..."
|
||||||
|
|
||||||
|
# Check if admin user already exists
|
||||||
|
EXISTING=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$EXISTING" -gt "0" ]; then
|
||||||
|
echo "User '$USERNAME' already exists."
|
||||||
|
echo "Resetting password and ensuring active status..."
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "UPDATE users SET password_hash='$PASSWORD_HASH', is_active=true WHERE username='$USERNAME';"
|
||||||
|
USER_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ')
|
||||||
|
else
|
||||||
|
echo "Creating new admin user..."
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "INSERT INTO users (username, password_hash, is_active, auth_provider, created_at, updated_at) VALUES ('$USERNAME', '$PASSWORD_HASH', true, 'local', NOW(), NOW());"
|
||||||
|
USER_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ')
|
||||||
|
echo "Admin user created successfully."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the Admin role ID (it's a system role)
|
||||||
|
ADMIN_ROLE_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM roles WHERE name='Admin';" 2>/dev/null | tr -d ' ')
|
||||||
|
|
||||||
|
if [ -z "$ADMIN_ROLE_ID" ]; then
|
||||||
|
echo "Warning: Admin role not found in database."
|
||||||
|
echo "The user was created but may not have admin privileges."
|
||||||
|
echo "Please check Settings > Auth > Roles after logging in."
|
||||||
|
else
|
||||||
|
# Check if user already has Admin role
|
||||||
|
HAS_ROLE=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM user_roles WHERE user_id=$USER_ID AND role_id=$ADMIN_ROLE_ID;" 2>/dev/null | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$HAS_ROLE" -eq "0" ]; then
|
||||||
|
echo "Assigning Admin role..."
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "INSERT INTO user_roles (user_id, role_id, created_at) VALUES ($USER_ID, $ADMIN_ROLE_ID, NOW());"
|
||||||
|
echo "Admin role assigned."
|
||||||
|
else
|
||||||
|
echo "User already has Admin role."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Credentials:"
|
||||||
|
echo " Username: admin"
|
||||||
|
echo " Password: admin123"
|
||||||
|
echo ""
|
||||||
|
echo "WARNING: Change the password immediately after logging in!"
|
||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PostgreSQL: Emergency script to disable authentication
|
||||||
|
# Use this if you're locked out of Dockhand
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/disable-auth.sh
|
||||||
|
#
|
||||||
|
# Requires: DATABASE_URL environment variable
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Disable Authentication (PostgreSQL)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "This script will disable authentication,"
|
||||||
|
echo "allowing access to Dockhand without login."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check DATABASE_URL
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "Error: DATABASE_URL environment variable not set"
|
||||||
|
echo ""
|
||||||
|
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse DATABASE_URL
|
||||||
|
DB_URL="$DATABASE_URL"
|
||||||
|
DB_URL="${DB_URL#postgres://}"
|
||||||
|
DB_URL="${DB_URL#postgresql://}"
|
||||||
|
|
||||||
|
DB_USER="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PASS="${DB_URL%%@*}"
|
||||||
|
DB_URL="${DB_URL#*@}"
|
||||||
|
DB_HOST="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PORT="${DB_URL%%/*}"
|
||||||
|
DB_NAME="${DB_URL#*/}"
|
||||||
|
DB_NAME="${DB_NAME%%\?*}"
|
||||||
|
|
||||||
|
export PGPASSWORD="$DB_PASS"
|
||||||
|
|
||||||
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Disabling authentication..."
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "UPDATE auth_settings SET auth_enabled = false WHERE id = 1;"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Authentication disabled successfully."
|
||||||
|
echo "You can now access Dockhand without logging in."
|
||||||
|
echo ""
|
||||||
|
echo "Remember to re-enable authentication in Settings after regaining access."
|
||||||
|
else
|
||||||
|
echo "Error: Failed to disable authentication"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PostgreSQL: Emergency script to list all users
|
||||||
|
# Shows username, admin status, active status, and last login
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/list-users.sh
|
||||||
|
#
|
||||||
|
# Requires: DATABASE_URL environment variable
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - List Users (PostgreSQL)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check DATABASE_URL
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "Error: DATABASE_URL environment variable not set"
|
||||||
|
echo ""
|
||||||
|
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse DATABASE_URL
|
||||||
|
DB_URL="$DATABASE_URL"
|
||||||
|
DB_URL="${DB_URL#postgres://}"
|
||||||
|
DB_URL="${DB_URL#postgresql://}"
|
||||||
|
|
||||||
|
DB_USER="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PASS="${DB_URL%%@*}"
|
||||||
|
DB_URL="${DB_URL#*@}"
|
||||||
|
DB_HOST="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PORT="${DB_URL%%/*}"
|
||||||
|
DB_NAME="${DB_URL#*/}"
|
||||||
|
DB_NAME="${DB_NAME%%\?*}"
|
||||||
|
|
||||||
|
export PGPASSWORD="$DB_PASS"
|
||||||
|
|
||||||
|
# Get user count
|
||||||
|
USER_COUNT=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM users;" 2>/dev/null | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$USER_COUNT" -eq "0" ]; then
|
||||||
|
echo "No users found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get Admin role ID for checking admin status
|
||||||
|
ADMIN_ROLE_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM roles WHERE name='Admin';" 2>/dev/null | tr -d ' ')
|
||||||
|
|
||||||
|
# Print header
|
||||||
|
printf "%-4s %-20s %-8s %-8s %-6s %s\n" "ID" "Username" "Admin" "Active" "MFA" "Last Login"
|
||||||
|
printf "%-4s %-20s %-8s %-8s %-6s %s\n" "----" "--------------------" "--------" "--------" "------" "-------------------"
|
||||||
|
|
||||||
|
# List users (check admin status via user_roles table)
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -A -F '|' -c "SELECT id, username, is_active, mfa_enabled, COALESCE(last_login::text, 'Never') FROM users ORDER BY id;" 2>/dev/null | while IFS='|' read id username is_active mfa_enabled last_login; do
|
||||||
|
# Check if user has Admin role
|
||||||
|
if [ -n "$ADMIN_ROLE_ID" ]; then
|
||||||
|
HAS_ADMIN=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM user_roles WHERE user_id=$id AND role_id=$ADMIN_ROLE_ID;" 2>/dev/null | tr -d ' ')
|
||||||
|
if [ "$HAS_ADMIN" -gt "0" ]; then
|
||||||
|
admin_str="Yes"
|
||||||
|
else
|
||||||
|
admin_str="No"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
admin_str="N/A"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert boolean values (PostgreSQL returns t/f)
|
||||||
|
if [ "$is_active" = "t" ]; then
|
||||||
|
active_str="Yes"
|
||||||
|
else
|
||||||
|
active_str="No"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$mfa_enabled" = "t" ]; then
|
||||||
|
mfa_str="Yes"
|
||||||
|
else
|
||||||
|
mfa_str="No"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%-4s %-20s %-8s %-8s %-6s %s\n" "$id" "$username" "$admin_str" "$active_str" "$mfa_str" "$last_login"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Total: $USER_COUNT user(s)"
|
||||||
|
|
||||||
|
# Show session count
|
||||||
|
SESSION_COUNT=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM sessions;" 2>/dev/null | tr -d ' ')
|
||||||
|
echo "Active sessions: $SESSION_COUNT"
|
||||||
Executable
+118
@@ -0,0 +1,118 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PostgreSQL: Emergency script to factory reset the database
|
||||||
|
# WARNING: This will DELETE ALL DATA including users, settings, and activity logs!
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/reset-db.sh
|
||||||
|
#
|
||||||
|
# Requires: DATABASE_URL environment variable
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Factory Reset Database (PostgreSQL)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "WARNING: This will DELETE ALL DATA!"
|
||||||
|
echo ""
|
||||||
|
echo "This includes:"
|
||||||
|
echo " - All users and their settings"
|
||||||
|
echo " - All sessions"
|
||||||
|
echo " - Authentication settings"
|
||||||
|
echo " - Activity logs"
|
||||||
|
echo " - Environment configurations"
|
||||||
|
echo " - OIDC/SSO settings"
|
||||||
|
echo ""
|
||||||
|
echo "The database tables will be truncated."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check DATABASE_URL
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "Error: DATABASE_URL environment variable not set"
|
||||||
|
echo ""
|
||||||
|
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse DATABASE_URL
|
||||||
|
DB_URL="$DATABASE_URL"
|
||||||
|
DB_URL="${DB_URL#postgres://}"
|
||||||
|
DB_URL="${DB_URL#postgresql://}"
|
||||||
|
|
||||||
|
DB_USER="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PASS="${DB_URL%%@*}"
|
||||||
|
DB_URL="${DB_URL#*@}"
|
||||||
|
DB_HOST="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PORT="${DB_URL%%/*}"
|
||||||
|
DB_NAME="${DB_URL#*/}"
|
||||||
|
DB_NAME="${DB_NAME%%\?*}"
|
||||||
|
|
||||||
|
export PGPASSWORD="$DB_PASS"
|
||||||
|
|
||||||
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Creating backup before reset..."
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
BACKUP_FILE="/app/data/dockhand_backup_pre_reset_$TIMESTAMP.sql"
|
||||||
|
if command -v pg_dump >/dev/null 2>&1; then
|
||||||
|
pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -F p -f "$BACKUP_FILE" 2>/dev/null || true
|
||||||
|
if [ -f "$BACKUP_FILE" ]; then
|
||||||
|
echo "Backup saved to: $BACKUP_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Truncating all tables..."
|
||||||
|
|
||||||
|
# Truncate all tables in the correct order (respecting foreign keys)
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" <<EOF
|
||||||
|
TRUNCATE TABLE
|
||||||
|
sessions,
|
||||||
|
user_roles,
|
||||||
|
dashboard_preferences,
|
||||||
|
audit_logs,
|
||||||
|
container_events,
|
||||||
|
vulnerability_scans,
|
||||||
|
stack_sources,
|
||||||
|
git_stacks,
|
||||||
|
git_repositories,
|
||||||
|
git_credentials,
|
||||||
|
host_metrics,
|
||||||
|
stack_events,
|
||||||
|
environment_notifications,
|
||||||
|
auto_update_settings,
|
||||||
|
users,
|
||||||
|
roles,
|
||||||
|
oidc_config,
|
||||||
|
ldap_config,
|
||||||
|
auth_settings,
|
||||||
|
notification_settings,
|
||||||
|
config_sets,
|
||||||
|
registries,
|
||||||
|
environments,
|
||||||
|
settings
|
||||||
|
CASCADE;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Database reset successfully."
|
||||||
|
echo ""
|
||||||
|
echo "Restart Dockhand to recreate default data:"
|
||||||
|
echo " docker restart dockhand"
|
||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PostgreSQL: Emergency script to reset a user's password
|
||||||
|
# Use this if a user is locked out and needs a password reset
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/reset-password.sh <username> <new_password>
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/reset-password.sh admin MyNewPassword123
|
||||||
|
#
|
||||||
|
# Requires: DATABASE_URL environment variable
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Reset User Password (PostgreSQL)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check arguments
|
||||||
|
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||||
|
echo "Usage: $0 <username> <new_password>"
|
||||||
|
echo ""
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 admin MyNewPassword123"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
USERNAME="$1"
|
||||||
|
NEW_PASSWORD="$2"
|
||||||
|
|
||||||
|
# Validate password length
|
||||||
|
if [ ${#NEW_PASSWORD} -lt 8 ]; then
|
||||||
|
echo "Error: Password must be at least 8 characters"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check DATABASE_URL
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "Error: DATABASE_URL environment variable not set"
|
||||||
|
echo ""
|
||||||
|
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse DATABASE_URL
|
||||||
|
DB_URL="$DATABASE_URL"
|
||||||
|
DB_URL="${DB_URL#postgres://}"
|
||||||
|
DB_URL="${DB_URL#postgresql://}"
|
||||||
|
|
||||||
|
DB_USER="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PASS="${DB_URL%%@*}"
|
||||||
|
DB_URL="${DB_URL#*@}"
|
||||||
|
DB_HOST="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PORT="${DB_URL%%/*}"
|
||||||
|
DB_NAME="${DB_URL#*/}"
|
||||||
|
DB_NAME="${DB_NAME%%\?*}"
|
||||||
|
|
||||||
|
export PGPASSWORD="$DB_PASS"
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
EXISTING=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$EXISTING" -eq "0" ]; then
|
||||||
|
echo "Error: User '$USERNAME' not found"
|
||||||
|
echo ""
|
||||||
|
echo "Available users:"
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT username FROM users;" 2>/dev/null | while read user; do
|
||||||
|
user=$(echo "$user" | tr -d ' ')
|
||||||
|
if [ -n "$user" ]; then
|
||||||
|
echo " - $user"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "This script will reset the password for user '$USERNAME'."
|
||||||
|
echo ""
|
||||||
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||||
|
echo "Username: $USERNAME"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Generate password hash using node (argon2 is available in the app)
|
||||||
|
echo ""
|
||||||
|
echo "Generating password hash..."
|
||||||
|
|
||||||
|
# Check if node and argon2 are available
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
# Try to use argon2 from node_modules
|
||||||
|
PASSWORD_HASH=$(node -e "
|
||||||
|
try {
|
||||||
|
const argon2 = require('argon2');
|
||||||
|
argon2.hash('$NEW_PASSWORD').then(h => console.log(h)).catch(e => process.exit(1));
|
||||||
|
} catch(e) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$PASSWORD_HASH" ]; then
|
||||||
|
echo "Error: Could not generate password hash (argon2 not available)"
|
||||||
|
echo "This script requires Node.js with argon2 module"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Error: Node.js is required to generate password hash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Resetting password for user '$USERNAME'..."
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "UPDATE users SET password_hash='$PASSWORD_HASH', updated_at=NOW() WHERE username='$USERNAME';"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Password reset successfully for user '$USERNAME'"
|
||||||
|
echo ""
|
||||||
|
# Invalidate sessions
|
||||||
|
USER_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ')
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "DELETE FROM sessions WHERE user_id=$USER_ID;" 2>/dev/null || true
|
||||||
|
echo "All existing sessions have been invalidated."
|
||||||
|
echo "The user can now log in with the new password."
|
||||||
|
else
|
||||||
|
echo "Error: Failed to reset password"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+117
@@ -0,0 +1,117 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PostgreSQL: Emergency script to restore the database from a backup
|
||||||
|
# WARNING: This will overwrite the current database!
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/restore-db.sh <backup_file>
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/postgres/restore-db.sh /app/data/dockhand_backup_20240115_120000.sql
|
||||||
|
#
|
||||||
|
# To copy backup into container first:
|
||||||
|
# docker cp ./dockhand_backup.sql dockhand:/app/data/
|
||||||
|
#
|
||||||
|
# Requires: DATABASE_URL environment variable
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Restore Database (PostgreSQL)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check argument
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Usage: $0 <backup_file>"
|
||||||
|
echo ""
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 /app/data/dockhand_backup_20240115_120000.sql"
|
||||||
|
echo ""
|
||||||
|
echo "To copy backup into container first:"
|
||||||
|
echo " docker cp ./dockhand_backup.sql dockhand:/app/data/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BACKUP_FILE="$1"
|
||||||
|
|
||||||
|
# Check DATABASE_URL
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "Error: DATABASE_URL environment variable not set"
|
||||||
|
echo ""
|
||||||
|
echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse DATABASE_URL
|
||||||
|
DB_URL="$DATABASE_URL"
|
||||||
|
DB_URL="${DB_URL#postgres://}"
|
||||||
|
DB_URL="${DB_URL#postgresql://}"
|
||||||
|
|
||||||
|
DB_USER="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PASS="${DB_URL%%@*}"
|
||||||
|
DB_URL="${DB_URL#*@}"
|
||||||
|
DB_HOST="${DB_URL%%:*}"
|
||||||
|
DB_URL="${DB_URL#*:}"
|
||||||
|
DB_PORT="${DB_URL%%/*}"
|
||||||
|
DB_NAME="${DB_URL#*/}"
|
||||||
|
DB_NAME="${DB_NAME%%\?*}"
|
||||||
|
|
||||||
|
export PGPASSWORD="$DB_PASS"
|
||||||
|
|
||||||
|
# Check if backup file exists
|
||||||
|
if [ ! -f "$BACKUP_FILE" ]; then
|
||||||
|
echo "Error: Backup file not found: $BACKUP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get backup file size
|
||||||
|
BACKUP_SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}')
|
||||||
|
|
||||||
|
echo "WARNING: This will overwrite the current database!"
|
||||||
|
echo ""
|
||||||
|
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
|
||||||
|
echo "Backup to restore: $BACKUP_FILE ($BACKUP_SIZE)"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Create backup of current database before restoring
|
||||||
|
echo ""
|
||||||
|
echo "Creating backup of current database..."
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
PRE_RESTORE_BACKUP="/app/data/dockhand_pre_restore_$TIMESTAMP.sql"
|
||||||
|
if command -v pg_dump >/dev/null 2>&1; then
|
||||||
|
pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -F p -f "$PRE_RESTORE_BACKUP" 2>/dev/null || true
|
||||||
|
if [ -f "$PRE_RESTORE_BACKUP" ]; then
|
||||||
|
echo "Current database backed up to: $PRE_RESTORE_BACKUP"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Restoring database..."
|
||||||
|
|
||||||
|
# Drop and recreate all tables by running the backup
|
||||||
|
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$BACKUP_FILE"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Database restored successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Restart Dockhand to apply changes:"
|
||||||
|
echo " docker restart dockhand"
|
||||||
|
else
|
||||||
|
echo "Error: Failed to restore database"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to factory reset the database
|
||||||
|
# Automatically detects database type (SQLite or PostgreSQL)
|
||||||
|
# WARNING: This will DELETE ALL DATA!
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/reset-db.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# Detect database type
|
||||||
|
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||||
|
exec "$SCRIPT_DIR/postgres/reset-db.sh" "$@"
|
||||||
|
else
|
||||||
|
exec "$SCRIPT_DIR/sqlite/reset-db.sh" "$@"
|
||||||
|
fi
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to reset a user's password
|
||||||
|
# Automatically detects database type (SQLite or PostgreSQL)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/reset-password.sh <username> <new_password>
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/reset-password.sh admin MyNewPassword123
|
||||||
|
#
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# Detect database type
|
||||||
|
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||||
|
exec "$SCRIPT_DIR/postgres/reset-password.sh" "$@"
|
||||||
|
else
|
||||||
|
exec "$SCRIPT_DIR/sqlite/reset-password.sh" "$@"
|
||||||
|
fi
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Emergency script to restore the database from a backup
|
||||||
|
# Automatically detects database type (SQLite or PostgreSQL)
|
||||||
|
# WARNING: This will overwrite the current database!
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/restore-db.sh <backup_file>
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/restore-db.sh /app/data/dockhand_backup_20240115_120000.db
|
||||||
|
#
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# Detect database type
|
||||||
|
if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then
|
||||||
|
exec "$SCRIPT_DIR/postgres/restore-db.sh" "$@"
|
||||||
|
else
|
||||||
|
exec "$SCRIPT_DIR/sqlite/restore-db.sh" "$@"
|
||||||
|
fi
|
||||||
Executable
+88
@@ -0,0 +1,88 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SQLite: Emergency script to backup the database
|
||||||
|
# Creates a timestamped copy of the database file
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/backup-db.sh [output_dir]
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/backup-db.sh /app/data/backups
|
||||||
|
#
|
||||||
|
# Default output: /app/data (same directory as database)
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Backup Database (SQLite)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Default database path
|
||||||
|
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||||
|
OUTPUT_DIR="${1:-$(dirname "$DB_PATH")}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||||
|
DB_PATH="./data/db/dockhand.db"
|
||||||
|
OUTPUT_DIR="${1:-./data/db}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$DB_PATH" ]; then
|
||||||
|
echo "Error: Database not found at $DB_PATH"
|
||||||
|
echo "Set DOCKHAND_DB environment variable to specify the database path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate backup filename with timestamp
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
BACKUP_FILE="$OUTPUT_DIR/dockhand_backup_$TIMESTAMP.db"
|
||||||
|
|
||||||
|
# Get database size
|
||||||
|
DB_SIZE=$(ls -lh "$DB_PATH" | awk '{print $5}')
|
||||||
|
|
||||||
|
echo "This script will create a backup of the database."
|
||||||
|
echo ""
|
||||||
|
echo "Source: $DB_PATH ($DB_SIZE)"
|
||||||
|
echo "Backup: $BACKUP_FILE"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create output directory if needed
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
echo "Creating database backup..."
|
||||||
|
|
||||||
|
# Use sqlite3 backup command for safe backup (handles WAL mode)
|
||||||
|
if command -v sqlite3 >/dev/null 2>&1; then
|
||||||
|
sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'"
|
||||||
|
else
|
||||||
|
# Fallback to file copy if sqlite3 not available
|
||||||
|
cp "$DB_PATH" "$BACKUP_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -eq 0 ] && [ -f "$BACKUP_FILE" ]; then
|
||||||
|
SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}')
|
||||||
|
echo ""
|
||||||
|
echo "Backup created successfully!"
|
||||||
|
echo "Size: $SIZE"
|
||||||
|
echo ""
|
||||||
|
echo "To copy from Docker container to host:"
|
||||||
|
echo " docker cp dockhand:$BACKUP_FILE ./dockhand_backup_$TIMESTAMP.db"
|
||||||
|
else
|
||||||
|
echo "Error: Failed to create backup"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SQLite: Emergency script to clear all user sessions
|
||||||
|
# Use this to force all users to re-login
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/clear-sessions.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Clear All Sessions (SQLite)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "This script will clear all user sessions,"
|
||||||
|
echo "forcing all users to log in again."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Default database path
|
||||||
|
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||||
|
DB_PATH="./data/db/dockhand.db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$DB_PATH" ]; then
|
||||||
|
echo "Error: Database not found at $DB_PATH"
|
||||||
|
echo "Set DOCKHAND_DB environment variable to specify the database path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM sessions;")
|
||||||
|
|
||||||
|
echo "Database: $DB_PATH"
|
||||||
|
echo "Active sessions: $COUNT"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Clearing all user sessions..."
|
||||||
|
sqlite3 "$DB_PATH" "DELETE FROM sessions;"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Cleared $COUNT session(s) successfully."
|
||||||
|
echo "All users will need to log in again."
|
||||||
|
else
|
||||||
|
echo "Error: Failed to clear sessions"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SQLite: Emergency script to create an admin user
|
||||||
|
# Use this if you're locked out of Dockhand and need to create a new admin
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/create-admin.sh
|
||||||
|
#
|
||||||
|
# Default credentials: admin / admin123
|
||||||
|
# CHANGE THE PASSWORD IMMEDIATELY after logging in!
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Create Admin User (SQLite)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "This script will create an admin user with:"
|
||||||
|
echo " Username: admin"
|
||||||
|
echo " Password: admin123"
|
||||||
|
echo ""
|
||||||
|
echo "If user 'admin' already exists, password will"
|
||||||
|
echo "be reset and admin privileges restored."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Default database path
|
||||||
|
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||||
|
DB_PATH="./data/db/dockhand.db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$DB_PATH" ]; then
|
||||||
|
echo "Error: Database not found at $DB_PATH"
|
||||||
|
echo "Set DOCKHAND_DB environment variable to specify the database path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Database: $DB_PATH"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Username and password
|
||||||
|
USERNAME="admin"
|
||||||
|
# Password: admin123
|
||||||
|
# This is an argon2id hash of "admin123" - generated with default argon2 settings
|
||||||
|
PASSWORD_HASH='$argon2id$v=19$m=65536,t=3,p=4$Jq4am2SfyYKmc0PAHe+yzg$cq/27vK/Qg2eZb/jMDy0ExLDhOG+58cKAximxpG5Dss'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Creating admin user..."
|
||||||
|
|
||||||
|
# Check if admin user already exists
|
||||||
|
EXISTING=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM users WHERE username='$USERNAME';")
|
||||||
|
|
||||||
|
if [ "$EXISTING" -gt "0" ]; then
|
||||||
|
echo "User '$USERNAME' already exists."
|
||||||
|
echo "Resetting password and ensuring active status..."
|
||||||
|
sqlite3 "$DB_PATH" "UPDATE users SET password_hash='$PASSWORD_HASH', is_active=1 WHERE username='$USERNAME';"
|
||||||
|
USER_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM users WHERE username='$USERNAME';")
|
||||||
|
else
|
||||||
|
echo "Creating new admin user..."
|
||||||
|
sqlite3 "$DB_PATH" "INSERT INTO users (username, password_hash, is_active, auth_provider, created_at, updated_at) VALUES ('$USERNAME', '$PASSWORD_HASH', 1, 'local', datetime('now'), datetime('now'));"
|
||||||
|
USER_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM users WHERE username='$USERNAME';")
|
||||||
|
echo "Admin user created successfully."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the Admin role ID (it's a system role)
|
||||||
|
ADMIN_ROLE_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM roles WHERE name='Admin';")
|
||||||
|
|
||||||
|
if [ -z "$ADMIN_ROLE_ID" ]; then
|
||||||
|
echo "Warning: Admin role not found in database."
|
||||||
|
echo "The user was created but may not have admin privileges."
|
||||||
|
echo "Please check Settings > Auth > Roles after logging in."
|
||||||
|
else
|
||||||
|
# Check if user already has Admin role
|
||||||
|
HAS_ROLE=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM user_roles WHERE user_id=$USER_ID AND role_id=$ADMIN_ROLE_ID;")
|
||||||
|
|
||||||
|
if [ "$HAS_ROLE" -eq "0" ]; then
|
||||||
|
echo "Assigning Admin role..."
|
||||||
|
sqlite3 "$DB_PATH" "INSERT INTO user_roles (user_id, role_id, created_at) VALUES ($USER_ID, $ADMIN_ROLE_ID, datetime('now'));"
|
||||||
|
echo "Admin role assigned."
|
||||||
|
else
|
||||||
|
echo "User already has Admin role."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Credentials:"
|
||||||
|
echo " Username: admin"
|
||||||
|
echo " Password: admin123"
|
||||||
|
echo ""
|
||||||
|
echo "WARNING: Change the password immediately after logging in!"
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SQLite: Emergency script to disable authentication
|
||||||
|
# Use this if you're locked out of Dockhand
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/disable-auth.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Disable Authentication (SQLite)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "This script will disable authentication,"
|
||||||
|
echo "allowing access to Dockhand without login."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Default database path
|
||||||
|
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||||
|
DB_PATH="./data/db/dockhand.db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$DB_PATH" ]; then
|
||||||
|
echo "Error: Database not found at $DB_PATH"
|
||||||
|
echo "Set DOCKHAND_DB environment variable to specify the database path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Database: $DB_PATH"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Disabling authentication..."
|
||||||
|
sqlite3 "$DB_PATH" "UPDATE auth_settings SET auth_enabled = 0 WHERE id = 1;"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Authentication disabled successfully."
|
||||||
|
echo "You can now access Dockhand without logging in."
|
||||||
|
echo ""
|
||||||
|
echo "Remember to re-enable authentication in Settings after regaining access."
|
||||||
|
else
|
||||||
|
echo "Error: Failed to disable authentication"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SQLite: Emergency script to list all users
|
||||||
|
# Shows username, admin status, active status, and last login
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/list-users.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - List Users (SQLite)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Default database path
|
||||||
|
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||||
|
DB_PATH="./data/db/dockhand.db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$DB_PATH" ]; then
|
||||||
|
echo "Error: Database not found at $DB_PATH"
|
||||||
|
echo "Set DOCKHAND_DB environment variable to specify the database path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get user count
|
||||||
|
USER_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM users;")
|
||||||
|
|
||||||
|
if [ "$USER_COUNT" -eq "0" ]; then
|
||||||
|
echo "No users found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get Admin role ID for checking admin status
|
||||||
|
ADMIN_ROLE_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM roles WHERE name='Admin';" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Print header
|
||||||
|
printf "%-4s %-20s %-8s %-8s %-6s %s\n" "ID" "Username" "Admin" "Active" "MFA" "Last Login"
|
||||||
|
printf "%-4s %-20s %-8s %-8s %-6s %s\n" "----" "--------------------" "--------" "--------" "------" "-------------------"
|
||||||
|
|
||||||
|
# List users (check admin status via user_roles table)
|
||||||
|
sqlite3 -separator '|' "$DB_PATH" "SELECT id, username, is_active, mfa_enabled, COALESCE(last_login, 'Never') FROM users ORDER BY id;" | while IFS='|' read id username is_active mfa_enabled last_login; do
|
||||||
|
# Check if user has Admin role
|
||||||
|
if [ -n "$ADMIN_ROLE_ID" ]; then
|
||||||
|
HAS_ADMIN=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM user_roles WHERE user_id=$id AND role_id=$ADMIN_ROLE_ID;")
|
||||||
|
if [ "$HAS_ADMIN" -gt "0" ]; then
|
||||||
|
admin_str="Yes"
|
||||||
|
else
|
||||||
|
admin_str="No"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
admin_str="N/A"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$is_active" = "1" ]; then
|
||||||
|
active_str="Yes"
|
||||||
|
else
|
||||||
|
active_str="No"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$mfa_enabled" = "1" ]; then
|
||||||
|
mfa_str="Yes"
|
||||||
|
else
|
||||||
|
mfa_str="No"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%-4s %-20s %-8s %-8s %-6s %s\n" "$id" "$username" "$admin_str" "$active_str" "$mfa_str" "$last_login"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Total: $USER_COUNT user(s)"
|
||||||
|
|
||||||
|
# Show session count
|
||||||
|
SESSION_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM sessions;")
|
||||||
|
echo "Active sessions: $SESSION_COUNT"
|
||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SQLite: Emergency script to factory reset the database
|
||||||
|
# WARNING: This will DELETE ALL DATA including users, settings, and activity logs!
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/reset-db.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Factory Reset Database (SQLite)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "WARNING: This will DELETE ALL DATA!"
|
||||||
|
echo ""
|
||||||
|
echo "This includes:"
|
||||||
|
echo " - All users and their settings"
|
||||||
|
echo " - All sessions"
|
||||||
|
echo " - Authentication settings"
|
||||||
|
echo " - Activity logs"
|
||||||
|
echo " - Environment configurations"
|
||||||
|
echo " - OIDC/SSO settings"
|
||||||
|
echo ""
|
||||||
|
echo "The database will be recreated on next startup."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Default database path
|
||||||
|
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||||
|
DB_PATH="./data/db/dockhand.db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$DB_PATH" ]; then
|
||||||
|
echo "Error: Database not found at $DB_PATH"
|
||||||
|
echo "Nothing to reset."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Database: $DB_PATH"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Creating backup before reset..."
|
||||||
|
BACKUP_FILE="${DB_PATH}.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
cp "$DB_PATH" "$BACKUP_FILE"
|
||||||
|
echo "Backup saved to: $BACKUP_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Deleting database..."
|
||||||
|
rm -f "$DB_PATH"
|
||||||
|
rm -f "${DB_PATH}-wal"
|
||||||
|
rm -f "${DB_PATH}-shm"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Database deleted successfully."
|
||||||
|
echo ""
|
||||||
|
echo "Restart Dockhand to recreate a fresh database:"
|
||||||
|
echo " docker restart dockhand"
|
||||||
Executable
+123
@@ -0,0 +1,123 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SQLite: Emergency script to reset a user's password
|
||||||
|
# Use this if a user is locked out and needs a password reset
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/reset-password.sh <username> <new_password>
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/reset-password.sh admin MyNewPassword123
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Reset User Password (SQLite)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check arguments
|
||||||
|
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||||
|
echo "Usage: $0 <username> <new_password>"
|
||||||
|
echo ""
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 admin MyNewPassword123"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
USERNAME="$1"
|
||||||
|
NEW_PASSWORD="$2"
|
||||||
|
|
||||||
|
# Validate password length
|
||||||
|
if [ ${#NEW_PASSWORD} -lt 8 ]; then
|
||||||
|
echo "Error: Password must be at least 8 characters"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Default database path
|
||||||
|
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||||
|
DB_PATH="./data/db/dockhand.db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$DB_PATH" ]; then
|
||||||
|
echo "Error: Database not found at $DB_PATH"
|
||||||
|
echo "Set DOCKHAND_DB environment variable to specify the database path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
EXISTING=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM users WHERE username='$USERNAME';")
|
||||||
|
|
||||||
|
if [ "$EXISTING" -eq "0" ]; then
|
||||||
|
echo "Error: User '$USERNAME' not found"
|
||||||
|
echo ""
|
||||||
|
echo "Available users:"
|
||||||
|
sqlite3 "$DB_PATH" "SELECT username FROM users;" | while read user; do
|
||||||
|
echo " - $user"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "This script will reset the password for user '$USERNAME'."
|
||||||
|
echo ""
|
||||||
|
echo "Database: $DB_PATH"
|
||||||
|
echo "Username: $USERNAME"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Generate password hash using node (argon2 is available in the app)
|
||||||
|
echo ""
|
||||||
|
echo "Generating password hash..."
|
||||||
|
|
||||||
|
# Check if node and argon2 are available
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
# Try to use argon2 from node_modules
|
||||||
|
PASSWORD_HASH=$(node -e "
|
||||||
|
try {
|
||||||
|
const argon2 = require('argon2');
|
||||||
|
argon2.hash('$NEW_PASSWORD').then(h => console.log(h)).catch(e => process.exit(1));
|
||||||
|
} catch(e) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$PASSWORD_HASH" ]; then
|
||||||
|
echo "Error: Could not generate password hash (argon2 not available)"
|
||||||
|
echo "This script requires Node.js with argon2 module"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Error: Node.js is required to generate password hash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Resetting password for user '$USERNAME'..."
|
||||||
|
sqlite3 "$DB_PATH" "UPDATE users SET password_hash='$PASSWORD_HASH', updated_at=datetime('now') WHERE username='$USERNAME';"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Password reset successfully for user '$USERNAME'"
|
||||||
|
echo ""
|
||||||
|
# Invalidate sessions
|
||||||
|
USER_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM users WHERE username='$USERNAME';")
|
||||||
|
sqlite3 "$DB_PATH" "DELETE FROM sessions WHERE user_id=$USER_ID;" 2>/dev/null || true
|
||||||
|
echo "All existing sessions have been invalidated."
|
||||||
|
echo "The user can now log in with the new password."
|
||||||
|
else
|
||||||
|
echo "Error: Failed to reset password"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+106
@@ -0,0 +1,106 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# SQLite: Emergency script to restore the database from a backup
|
||||||
|
# WARNING: This will overwrite the current database!
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/restore-db.sh <backup_file>
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# docker exec -it dockhand /app/scripts/emergency/sqlite/restore-db.sh /app/data/dockhand_backup_20240115_120000.db
|
||||||
|
#
|
||||||
|
# To copy backup into container first:
|
||||||
|
# docker cp ./dockhand_backup.db dockhand:/app/data/
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " Dockhand - Restore Database (SQLite)"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check argument
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Usage: $0 <backup_file>"
|
||||||
|
echo ""
|
||||||
|
echo "Example:"
|
||||||
|
echo " $0 /app/data/dockhand_backup_20240115_120000.db"
|
||||||
|
echo ""
|
||||||
|
echo "To copy backup into container first:"
|
||||||
|
echo " docker cp ./dockhand_backup.db dockhand:/app/data/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BACKUP_FILE="$1"
|
||||||
|
|
||||||
|
# Default database path
|
||||||
|
DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}"
|
||||||
|
|
||||||
|
# Check if running locally (not in Docker)
|
||||||
|
if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then
|
||||||
|
DB_PATH="./data/db/dockhand.db"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if backup file exists
|
||||||
|
if [ ! -f "$BACKUP_FILE" ]; then
|
||||||
|
echo "Error: Backup file not found: $BACKUP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify it's a valid SQLite database
|
||||||
|
if ! sqlite3 "$BACKUP_FILE" "SELECT 1;" >/dev/null 2>&1; then
|
||||||
|
echo "Error: File is not a valid SQLite database: $BACKUP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get backup file size
|
||||||
|
BACKUP_SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}')
|
||||||
|
|
||||||
|
echo "WARNING: This will overwrite the current database!"
|
||||||
|
echo ""
|
||||||
|
echo "Current database: $DB_PATH"
|
||||||
|
echo "Backup to restore: $BACKUP_FILE ($BACKUP_SIZE)"
|
||||||
|
echo ""
|
||||||
|
printf "Continue? [y/N]: "
|
||||||
|
read CONFIRM
|
||||||
|
|
||||||
|
case "$CONFIRM" in
|
||||||
|
[yY]|[yY][eE][sS])
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborted."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Create backup of current database before restoring
|
||||||
|
if [ -f "$DB_PATH" ]; then
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
PRE_RESTORE_BACKUP="${DB_PATH}.pre-restore.$TIMESTAMP"
|
||||||
|
echo ""
|
||||||
|
echo "Creating backup of current database..."
|
||||||
|
cp "$DB_PATH" "$PRE_RESTORE_BACKUP"
|
||||||
|
echo "Current database backed up to: $PRE_RESTORE_BACKUP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Restoring database..."
|
||||||
|
|
||||||
|
# Remove WAL files if they exist
|
||||||
|
rm -f "${DB_PATH}-wal"
|
||||||
|
rm -f "${DB_PATH}-shm"
|
||||||
|
|
||||||
|
# Copy backup to database location
|
||||||
|
cp "$BACKUP_FILE" "$DB_PATH"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Database restored successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Restart Dockhand to apply changes:"
|
||||||
|
echo " docker restart dockhand"
|
||||||
|
else
|
||||||
|
echo "Error: Failed to restore database"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Generate changelog section in webpage/index.html from src/lib/data/changelog.json
|
||||||
|
* This ensures a single source of truth for release information
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const ROOT_DIR = join(import.meta.dir, '..');
|
||||||
|
const CHANGELOG_PATH = join(ROOT_DIR, 'src/lib/data/changelog.json');
|
||||||
|
const INDEX_PATH = join(ROOT_DIR, 'webpage/index.html');
|
||||||
|
|
||||||
|
interface ChangelogEntry {
|
||||||
|
version: string;
|
||||||
|
date: string;
|
||||||
|
changes: Array<{ type: 'feature' | 'fix'; text: string }>;
|
||||||
|
imageTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVG icons for change types
|
||||||
|
const FEATURE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg>`;
|
||||||
|
|
||||||
|
const FIX_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="8" height="14" x="8" y="6" rx="4"/><path d="m19 7-3 2"/><path d="m5 7 3 2"/><path d="m19 19-3-2"/><path d="m5 19 3-2"/><path d="M20 13h-4"/><path d="M4 13h4"/><path d="m10 4 1 2"/><path d="m14 4-1 2"/></svg>`;
|
||||||
|
|
||||||
|
const TOGGLE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`;
|
||||||
|
|
||||||
|
const COPY_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
||||||
|
|
||||||
|
function formatDate(dateStr: string): string {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateChangeItem(change: { type: 'feature' | 'fix'; text: string }): string {
|
||||||
|
const pillClass = change.type === 'feature' ? 'changelog-pill-feature' : 'changelog-pill-fix';
|
||||||
|
const svg = change.type === 'feature' ? FEATURE_SVG : FIX_SVG;
|
||||||
|
const label = change.type === 'feature' ? 'New' : 'Fix';
|
||||||
|
return ` <li><span class="changelog-pill ${pillClass}">${svg}${label}</span>${change.text}</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateLatestEntry(entry: ChangelogEntry): string {
|
||||||
|
const changes = entry.changes.map(generateChangeItem).join('\n');
|
||||||
|
const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`;
|
||||||
|
|
||||||
|
return ` <!-- ${version} -->
|
||||||
|
<div class="changelog-entry">
|
||||||
|
<div class="changelog-header">
|
||||||
|
<div class="changelog-version">
|
||||||
|
<h3>${version}</h3>
|
||||||
|
<span class="changelog-badge">Latest</span>
|
||||||
|
</div>
|
||||||
|
<span class="changelog-date">${formatDate(entry.date)}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="changelog-changes">
|
||||||
|
${changes}
|
||||||
|
</ul>
|
||||||
|
<div class="changelog-image-tag">
|
||||||
|
<span>Docker image:</span>
|
||||||
|
<code>${entry.imageTag}</code>
|
||||||
|
<button class="copy-btn" onclick="copyDockerImage(this, '${entry.imageTag}')" title="Copy to clipboard">${COPY_SVG}</button>
|
||||||
|
<span style="color: var(--text-muted); margin: 0 0.25rem;">or</span>
|
||||||
|
<code>fnsys/dockhand:latest</code>
|
||||||
|
<button class="copy-btn" onclick="copyDockerImage(this, 'fnsys/dockhand:latest')" title="Copy to clipboard">${COPY_SVG}</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCollapsibleEntry(entry: ChangelogEntry): string {
|
||||||
|
const changes = entry.changes.map(generateChangeItem).join('\n');
|
||||||
|
const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`;
|
||||||
|
|
||||||
|
return ` <!-- ${version} (collapsible) -->
|
||||||
|
<div class="changelog-entry collapsible" data-version="${version}">
|
||||||
|
<div class="changelog-header">
|
||||||
|
<div class="changelog-version">
|
||||||
|
<h3>${version}</h3>
|
||||||
|
<span class="changelog-toggle">${TOGGLE_SVG}</span>
|
||||||
|
</div>
|
||||||
|
<span class="changelog-date">${formatDate(entry.date)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="changelog-content">
|
||||||
|
<ul class="changelog-changes">
|
||||||
|
${changes}
|
||||||
|
</ul>
|
||||||
|
<div class="changelog-image-tag">
|
||||||
|
<span>Docker image:</span>
|
||||||
|
<code>${entry.imageTag}</code>
|
||||||
|
<button class="copy-btn" onclick="copyDockerImage(this, '${entry.imageTag}')" title="Copy to clipboard">${COPY_SVG}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateChangelogSection(entries: ChangelogEntry[]): string {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const [latest, ...rest] = entries;
|
||||||
|
const latestHtml = generateLatestEntry(latest);
|
||||||
|
const restHtml = rest.map(generateCollapsibleEntry).join('\n');
|
||||||
|
|
||||||
|
return ` <!-- Changelog Section -->
|
||||||
|
<section class="changelog" id="changelog">
|
||||||
|
<div class="changelog-container">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-label">Changelog</div>
|
||||||
|
<h2 class="section-title">Release history</h2>
|
||||||
|
<p class="section-subtitle">Track our progress and see what's new in each version. <span style="color: #fbbf24; white-space: nowrap;">Spoiler: it gets better every time.</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="changelog-list">
|
||||||
|
${latestHtml}
|
||||||
|
${restHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read changelog.json
|
||||||
|
console.log('Reading changelog from:', CHANGELOG_PATH);
|
||||||
|
const changelog: ChangelogEntry[] = JSON.parse(readFileSync(CHANGELOG_PATH, 'utf-8'));
|
||||||
|
console.log(`Found ${changelog.length} changelog entries`);
|
||||||
|
|
||||||
|
// Read index.html
|
||||||
|
console.log('Reading index.html from:', INDEX_PATH);
|
||||||
|
let indexHtml = readFileSync(INDEX_PATH, 'utf-8');
|
||||||
|
|
||||||
|
// Generate new changelog section
|
||||||
|
const newChangelogSection = generateChangelogSection(changelog);
|
||||||
|
|
||||||
|
// Replace changelog section using regex
|
||||||
|
// Match from "<!-- Changelog Section -->" to the closing "</section>" before "<!-- CTA -->"
|
||||||
|
const changelogRegex = / <!-- Changelog Section -->[\s\S]*?<\/section>(?=\s*\n\s*<!-- CTA -->)/;
|
||||||
|
|
||||||
|
if (!changelogRegex.test(indexHtml)) {
|
||||||
|
console.error('ERROR: Could not find changelog section in index.html');
|
||||||
|
console.error('Looking for pattern: <!-- Changelog Section --> ... </section> followed by <!-- CTA -->');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
indexHtml = indexHtml.replace(changelogRegex, newChangelogSection);
|
||||||
|
|
||||||
|
// Also update softwareVersion in JSON-LD schema
|
||||||
|
if (changelog.length > 0) {
|
||||||
|
const latestVersion = changelog[0].version;
|
||||||
|
// Match "softwareVersion": "X.X" or "softwareVersion": "X.X.X"
|
||||||
|
const versionRegex = /"softwareVersion":\s*"[\d.]+"/;
|
||||||
|
if (versionRegex.test(indexHtml)) {
|
||||||
|
indexHtml = indexHtml.replace(versionRegex, `"softwareVersion": "${latestVersion}"`);
|
||||||
|
console.log(`Updated softwareVersion to: ${latestVersion}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to index.html
|
||||||
|
writeFileSync(INDEX_PATH, indexHtml);
|
||||||
|
console.log('');
|
||||||
|
console.log('Generated changelog in webpage/index.html');
|
||||||
|
console.log(` - Latest version: v${changelog[0]?.version || 'unknown'}`);
|
||||||
|
console.log(` - Total entries: ${changelog.length}`);
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Generate static HTML pages for License and Privacy from .txt files
|
||||||
|
* This ensures a single source of truth for legal documents
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const ROOT_DIR = join(import.meta.dir, '..');
|
||||||
|
const WEBPAGE_DIR = join(ROOT_DIR, 'webpage');
|
||||||
|
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateHtmlPage(title: string, content: string): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>${title} - Dockhand</title>
|
||||||
|
<link rel="icon" type="image/png" href="images/favicon.png">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background: #0a0a0f;
|
||||||
|
color: #e0e0e0;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 0;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.logo-img {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.back-link {
|
||||||
|
color: #60a5fa;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
color: #c0c0c0;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
margin-top: 3rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
footer a {
|
||||||
|
color: #60a5fa;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<a href="index.html">
|
||||||
|
<img src="images/logo-dark.webp" alt="Dockhand" class="logo-img">
|
||||||
|
</a>
|
||||||
|
<a href="index.html" class="back-link">← Back to home</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h1>${title}</h1>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<pre>${escapeHtml(content)}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2025-2026 Finsys / Jarek Krochmalski · <a href="https://dockhand.pro">https://dockhand.pro</a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the source files
|
||||||
|
const licenseContent = readFileSync(join(ROOT_DIR, 'LICENSE.txt'), 'utf-8');
|
||||||
|
const privacyContent = readFileSync(join(ROOT_DIR, 'PRIVACY.txt'), 'utf-8');
|
||||||
|
|
||||||
|
// Generate HTML pages
|
||||||
|
const licenseHtml = generateHtmlPage('License Terms and Conditions', licenseContent);
|
||||||
|
const privacyHtml = generateHtmlPage('Privacy Policy', privacyContent);
|
||||||
|
|
||||||
|
// Write to webpage directory
|
||||||
|
writeFileSync(join(WEBPAGE_DIR, 'license.html'), licenseHtml);
|
||||||
|
writeFileSync(join(WEBPAGE_DIR, 'privacy.html'), privacyHtml);
|
||||||
|
|
||||||
|
console.log('Generated legal pages:');
|
||||||
|
console.log(' - webpage/license.html');
|
||||||
|
console.log(' - webpage/privacy.html');
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
/**
|
||||||
|
* Post-build script to fix svelte-adapter-bun WebSocket issue
|
||||||
|
* The adapter calls server.websocket() which doesn't exist in SvelteKit.
|
||||||
|
*
|
||||||
|
* IMPORTANT: Terminal WebSocket logic is shared with vite.config.ts
|
||||||
|
* Core functions like resolveDockerTarget are defined in:
|
||||||
|
* src/lib/server/ws-terminal-shared.ts
|
||||||
|
*
|
||||||
|
* When updating WebSocket terminal handling, update the shared module
|
||||||
|
* and this file will use the same logic at build time.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
const BUILD_DIR = join(import.meta.dir, '../build');
|
||||||
|
|
||||||
|
async function patchHandler() {
|
||||||
|
const handlerPath = join(BUILD_DIR, 'handler.js');
|
||||||
|
const handlerFile = Bun.file(handlerPath);
|
||||||
|
|
||||||
|
if (!await handlerFile.exists()) {
|
||||||
|
console.error('handler.js not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = await handlerFile.text();
|
||||||
|
|
||||||
|
// Replace broken server.websocket() call
|
||||||
|
content = content.replace(
|
||||||
|
'const websocket = server.websocket();',
|
||||||
|
'const websocket = null;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add WebSocket upgrade detection before ssr handler
|
||||||
|
const ssrIndex = content.indexOf('var ssr = async (request, bunServer) => {');
|
||||||
|
if (ssrIndex > -1) {
|
||||||
|
const upgradeCode = `
|
||||||
|
var handleUpgrade = (request, bunServer) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const isUpgrade = request.headers.get('connection')?.toLowerCase().includes('upgrade') &&
|
||||||
|
request.headers.get('upgrade')?.toLowerCase() === 'websocket';
|
||||||
|
if (!isUpgrade) return null;
|
||||||
|
|
||||||
|
// Handle terminal exec WebSocket
|
||||||
|
if (url.pathname.includes('/api/containers/') && url.pathname.includes('/exec')) {
|
||||||
|
const pathParts = url.pathname.split('/');
|
||||||
|
const containerIdIndex = pathParts.indexOf('containers') + 1;
|
||||||
|
const containerId = pathParts[containerIdIndex];
|
||||||
|
const shell = url.searchParams.get('shell') || '/bin/sh';
|
||||||
|
const user = url.searchParams.get('user') || 'root';
|
||||||
|
const envId = url.searchParams.get('envId') ? parseInt(url.searchParams.get('envId'), 10) : undefined;
|
||||||
|
if (bunServer.upgrade(request, { data: { type: 'terminal', containerId, shell, user, envId } })) {
|
||||||
|
return new Response(null, { status: 101 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Hawser Edge WebSocket
|
||||||
|
if (url.pathname === '/api/hawser/connect') {
|
||||||
|
if (bunServer.upgrade(request, { data: { type: 'hawser' } })) {
|
||||||
|
return new Response(null, { status: 101 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
content = content.slice(0, ssrIndex) + upgradeCode + content.slice(ssrIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify handler to check for upgrade first
|
||||||
|
content = content.replace(
|
||||||
|
'return ssr(request, server2);',
|
||||||
|
'const upgradeResponse = handleUpgrade(request, server2); if (upgradeResponse) return upgradeResponse; return ssr(request, server2);'
|
||||||
|
);
|
||||||
|
|
||||||
|
await Bun.write(handlerPath, content);
|
||||||
|
console.log('✓ Patched handler.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function patchIndex() {
|
||||||
|
const indexPath = join(BUILD_DIR, 'index.js');
|
||||||
|
const indexFile = Bun.file(indexPath);
|
||||||
|
|
||||||
|
if (!await indexFile.exists()) {
|
||||||
|
console.error('index.js not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = await indexFile.text();
|
||||||
|
|
||||||
|
const wsHandler = `
|
||||||
|
import { existsSync as _existsSync } from 'fs';
|
||||||
|
import { homedir as _homedir } from 'os';
|
||||||
|
import { Database as _Database } from 'bun:sqlite';
|
||||||
|
import { SQL as _SQL } from 'bun';
|
||||||
|
import { join as _join } from 'path';
|
||||||
|
|
||||||
|
// Database connection (supports both SQLite and PostgreSQL)
|
||||||
|
let _db = null;
|
||||||
|
let _isPostgres = false;
|
||||||
|
function _getDb() {
|
||||||
|
if (!_db) {
|
||||||
|
const dbUrl = process.env.DATABASE_URL;
|
||||||
|
if (dbUrl && (dbUrl.startsWith('postgres://') || dbUrl.startsWith('postgresql://'))) {
|
||||||
|
_db = new _SQL(dbUrl);
|
||||||
|
_isPostgres = true;
|
||||||
|
} else {
|
||||||
|
const _dbPath = _join(process.cwd(), 'data', 'db', 'dockhand.db');
|
||||||
|
if (_existsSync(_dbPath)) {
|
||||||
|
_db = new _Database(_dbPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _getEnvironment(id) {
|
||||||
|
const db = _getDb();
|
||||||
|
if (!db) return null;
|
||||||
|
let row;
|
||||||
|
if (_isPostgres) {
|
||||||
|
const result = await db.unsafe('SELECT * FROM environments WHERE id = $1', [id]);
|
||||||
|
row = result[0];
|
||||||
|
} else {
|
||||||
|
row = db.prepare('SELECT * FROM environments WHERE id = ?').get(id);
|
||||||
|
}
|
||||||
|
return row ? { ...row, is_local: Boolean(row.is_local), connection_type: row.connection_type, hawser_token: row.hawser_token } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectDockerSocket() {
|
||||||
|
if (process.env.DOCKER_SOCKET && _existsSync(process.env.DOCKER_SOCKET)) return process.env.DOCKER_SOCKET;
|
||||||
|
if (process.env.DOCKER_HOST?.startsWith('unix://')) {
|
||||||
|
const p = process.env.DOCKER_HOST.replace('unix://', '');
|
||||||
|
if (_existsSync(p)) return p;
|
||||||
|
}
|
||||||
|
for (const s of ['/var/run/docker.sock', _homedir() + '/.docker/run/docker.sock', _homedir() + '/.orbstack/run/docker.sock', '/run/docker.sock']) {
|
||||||
|
if (_existsSync(s)) return s;
|
||||||
|
}
|
||||||
|
return '/var/run/docker.sock';
|
||||||
|
}
|
||||||
|
const dockerSocketPath = detectDockerSocket();
|
||||||
|
console.log('Detected Docker socket at:', dockerSocketPath);
|
||||||
|
|
||||||
|
const dockerStreams = new Map();
|
||||||
|
let _wsConnCounter = 0;
|
||||||
|
|
||||||
|
async function _getDockerTarget(envId) {
|
||||||
|
if (!envId) return { type: 'unix', socket: dockerSocketPath };
|
||||||
|
const env = await _getEnvironment(envId);
|
||||||
|
if (!env) return { type: 'unix', socket: dockerSocketPath };
|
||||||
|
// Check for socket connection type (local Unix socket)
|
||||||
|
if (env.is_local || env.connection_type === 'socket' || !env.connection_type) {
|
||||||
|
return { type: 'unix', socket: env.socket_path || dockerSocketPath };
|
||||||
|
}
|
||||||
|
if (env.connection_type === 'hawser-edge') return { type: 'hawser-edge', environmentId: envId };
|
||||||
|
return { type: 'tcp', host: env.host, port: env.port || 2375, hawserToken: env.connection_type === 'hawser-standard' ? env.hawser_token : undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createExec(containerId, cmd, user, target) {
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
const fetchOpts = {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: cmd, User: user })
|
||||||
|
};
|
||||||
|
let url;
|
||||||
|
if (target.type === 'unix') {
|
||||||
|
url = 'http://localhost/containers/' + containerId + '/exec';
|
||||||
|
fetchOpts.unix = target.socket;
|
||||||
|
} else {
|
||||||
|
url = 'http://' + target.host + ':' + target.port + '/containers/' + containerId + '/exec';
|
||||||
|
if (target.hawserToken) headers['X-Hawser-Token'] = target.hawserToken;
|
||||||
|
}
|
||||||
|
const res = await fetch(url, fetchOpts);
|
||||||
|
if (!res.ok) throw new Error('Failed to create exec: ' + (await res.text()));
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resizeExec(execId, cols, rows, target) {
|
||||||
|
try {
|
||||||
|
const fetchOpts = { method: 'POST' };
|
||||||
|
let url;
|
||||||
|
if (target.type === 'unix') {
|
||||||
|
url = 'http://localhost/exec/' + execId + '/resize?h=' + rows + '&w=' + cols;
|
||||||
|
fetchOpts.unix = target.socket;
|
||||||
|
} else {
|
||||||
|
url = 'http://' + target.host + ':' + target.port + '/exec/' + execId + '/resize?h=' + rows + '&w=' + cols;
|
||||||
|
if (target.hawserToken) fetchOpts.headers = { 'X-Hawser-Token': target.hawserToken };
|
||||||
|
}
|
||||||
|
await fetch(url, fetchOpts);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Hawser Edge Support ============
|
||||||
|
// Global edge connections map (shared with hawser.ts via globalThis)
|
||||||
|
if (!globalThis.__hawserEdgeConnections) globalThis.__hawserEdgeConnections = new Map();
|
||||||
|
const _edgeConnections = globalThis.__hawserEdgeConnections;
|
||||||
|
|
||||||
|
// Map WebSocket to environmentId for quick lookup
|
||||||
|
const _wsToEnvId = new Map();
|
||||||
|
|
||||||
|
// Edge exec sessions (execId -> frontend WebSocket)
|
||||||
|
const _edgeExecSessions = new Map();
|
||||||
|
|
||||||
|
// Validate Hawser token against database
|
||||||
|
async function _validateHawserToken(token) {
|
||||||
|
const db = _getDb();
|
||||||
|
if (!db) return { valid: false };
|
||||||
|
let tokens;
|
||||||
|
if (_isPostgres) {
|
||||||
|
tokens = await db.unsafe('SELECT * FROM hawser_tokens WHERE is_active = true');
|
||||||
|
} else {
|
||||||
|
tokens = db.prepare('SELECT * FROM hawser_tokens WHERE is_active = 1').all();
|
||||||
|
}
|
||||||
|
for (const t of tokens) {
|
||||||
|
try {
|
||||||
|
const isValid = await Bun.password.verify(token, t.token);
|
||||||
|
if (isValid) {
|
||||||
|
if (_isPostgres) {
|
||||||
|
await db.unsafe('UPDATE hawser_tokens SET last_used = NOW() WHERE id = $1', [t.id]);
|
||||||
|
} else {
|
||||||
|
db.prepare('UPDATE hawser_tokens SET last_used = datetime(\\'now\\') WHERE id = ?').run(t.id);
|
||||||
|
}
|
||||||
|
return { valid: true, environmentId: t.environment_id, tokenId: t.id };
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update environment status in database
|
||||||
|
async function _updateEnvStatus(envId, conn) {
|
||||||
|
const db = _getDb();
|
||||||
|
if (!db) return;
|
||||||
|
try {
|
||||||
|
if (conn) {
|
||||||
|
if (_isPostgres) {
|
||||||
|
await db.unsafe('UPDATE environments SET hawser_last_seen = NOW(), hawser_agent_id = $1, hawser_agent_name = $2, hawser_version = $3, hawser_capabilities = $4 WHERE id = $5',
|
||||||
|
[conn.agentId, conn.agentName, conn.agentVersion, JSON.stringify(conn.capabilities || []), envId]);
|
||||||
|
} else {
|
||||||
|
db.prepare('UPDATE environments SET hawser_last_seen = datetime(\\'now\\'), hawser_agent_id = ?, hawser_agent_name = ?, hawser_version = ?, hawser_capabilities = ? WHERE id = ?')
|
||||||
|
.run(conn.agentId, conn.agentName, conn.agentVersion, JSON.stringify(conn.capabilities || []), envId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_isPostgres) {
|
||||||
|
await db.unsafe('UPDATE environments SET hawser_last_seen = NOW() WHERE id = $1', [envId]);
|
||||||
|
} else {
|
||||||
|
db.prepare('UPDATE environments SET hawser_last_seen = datetime(\\'now\\') WHERE id = ?').run(envId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Hawser Edge protocol messages
|
||||||
|
async function _handleHawserMessage(ws, msg) {
|
||||||
|
if (msg.type === 'hello') {
|
||||||
|
console.log('[Hawser] Hello from agent:', msg.agentName, '(' + msg.agentId + ')');
|
||||||
|
const validation = await _validateHawserToken(msg.token);
|
||||||
|
if (!validation.valid) {
|
||||||
|
console.log('[Hawser] Invalid token');
|
||||||
|
ws.send(JSON.stringify({ type: 'error', error: 'Invalid token' }));
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const envId = validation.environmentId;
|
||||||
|
const existing = _edgeConnections.get(envId);
|
||||||
|
if (existing) {
|
||||||
|
const pendingCount = existing.pendingRequests.size;
|
||||||
|
const streamCount = existing.pendingStreamRequests.size;
|
||||||
|
console.log('[Hawser] Replacing existing connection for env', envId, '- rejecting', pendingCount, 'pending requests and', streamCount, 'stream requests');
|
||||||
|
// Reject all pending requests before closing
|
||||||
|
for (const [requestId, pending] of existing.pendingRequests) {
|
||||||
|
clearTimeout(pending.timeout);
|
||||||
|
pending.reject(new Error('Connection replaced by new agent'));
|
||||||
|
}
|
||||||
|
for (const [requestId, pending] of existing.pendingStreamRequests) {
|
||||||
|
pending.onEnd?.('Connection replaced by new agent');
|
||||||
|
}
|
||||||
|
existing.pendingRequests.clear();
|
||||||
|
existing.pendingStreamRequests.clear();
|
||||||
|
existing.ws.close(1000, 'Replaced');
|
||||||
|
_wsToEnvId.delete(existing.ws);
|
||||||
|
}
|
||||||
|
const conn = {
|
||||||
|
ws, environmentId: envId, agentId: msg.agentId, agentName: msg.agentName,
|
||||||
|
agentVersion: msg.version || 'unknown', dockerVersion: msg.dockerVersion || 'unknown',
|
||||||
|
hostname: msg.hostname || 'unknown', capabilities: msg.capabilities || [],
|
||||||
|
connectedAt: new Date(), lastHeartbeat: new Date(),
|
||||||
|
pendingRequests: new Map(), pendingStreamRequests: new Map(),
|
||||||
|
pingInterval: null
|
||||||
|
};
|
||||||
|
_edgeConnections.set(envId, conn);
|
||||||
|
_wsToEnvId.set(ws, envId);
|
||||||
|
await _updateEnvStatus(envId, conn);
|
||||||
|
ws.send(JSON.stringify({ type: 'welcome', environmentId: envId, message: 'Connected to Dockhand' }));
|
||||||
|
// Start server-side ping interval to keep connection alive through Traefik/proxies (5s)
|
||||||
|
conn.pingInterval = setInterval(() => {
|
||||||
|
try { ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); }
|
||||||
|
catch { if (conn.pingInterval) { clearInterval(conn.pingInterval); conn.pingInterval = null; } }
|
||||||
|
}, 5000);
|
||||||
|
console.log('[Hawser] Agent', msg.agentName, 'connected for env', envId);
|
||||||
|
} else if (msg.type === 'ping') {
|
||||||
|
const envId = _wsToEnvId.get(ws);
|
||||||
|
if (envId) { const c = _edgeConnections.get(envId); if (c) c.lastHeartbeat = new Date(); }
|
||||||
|
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
|
||||||
|
} else if (msg.type === 'pong') {
|
||||||
|
const envId = _wsToEnvId.get(ws);
|
||||||
|
if (envId) { const c = _edgeConnections.get(envId); if (c) c.lastHeartbeat = new Date(); }
|
||||||
|
} else if (msg.type === 'response') {
|
||||||
|
const envId = _wsToEnvId.get(ws);
|
||||||
|
if (!envId) {
|
||||||
|
console.warn('[Hawser] Response from unknown WebSocket, requestId=' + msg.requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conn = _edgeConnections.get(envId);
|
||||||
|
if (conn) {
|
||||||
|
const pending = conn.pendingRequests.get(msg.requestId);
|
||||||
|
if (pending) {
|
||||||
|
clearTimeout(pending.timeout);
|
||||||
|
conn.pendingRequests.delete(msg.requestId);
|
||||||
|
pending.resolve({ statusCode: msg.statusCode, headers: msg.headers || {}, body: msg.body || '', isBinary: msg.isBinary || false });
|
||||||
|
} else {
|
||||||
|
console.warn('[Hawser] Response for unknown request ' + msg.requestId + ' on env ' + envId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'stream') {
|
||||||
|
const envId = _wsToEnvId.get(ws);
|
||||||
|
if (!envId) {
|
||||||
|
console.warn('[Hawser] Stream data from unknown WebSocket, requestId=' + msg.requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conn = _edgeConnections.get(envId);
|
||||||
|
if (conn?.pendingStreamRequests) {
|
||||||
|
const pending = conn.pendingStreamRequests.get(msg.requestId);
|
||||||
|
if (pending) {
|
||||||
|
pending.onData(msg.data, msg.stream);
|
||||||
|
} else {
|
||||||
|
console.warn('[Hawser] Stream data for unknown request ' + msg.requestId + ' on env ' + envId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'stream_end') {
|
||||||
|
const envId = _wsToEnvId.get(ws);
|
||||||
|
if (!envId) {
|
||||||
|
console.warn('[Hawser] Stream end from unknown WebSocket, requestId=' + msg.requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conn = _edgeConnections.get(envId);
|
||||||
|
if (conn?.pendingStreamRequests) {
|
||||||
|
const pending = conn.pendingStreamRequests.get(msg.requestId);
|
||||||
|
if (pending) {
|
||||||
|
conn.pendingStreamRequests.delete(msg.requestId);
|
||||||
|
pending.onEnd(msg.reason);
|
||||||
|
} else {
|
||||||
|
console.warn('[Hawser] Stream end for unknown request ' + msg.requestId + ' on env ' + envId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'exec_ready') {
|
||||||
|
const session = _edgeExecSessions.get(msg.execId);
|
||||||
|
if (session?.ws?.readyState === 1) console.log('[Hawser] Exec ready:', msg.execId);
|
||||||
|
} else if (msg.type === 'exec_output') {
|
||||||
|
const session = _edgeExecSessions.get(msg.execId);
|
||||||
|
if (session?.ws?.readyState === 1) {
|
||||||
|
const data = Buffer.from(msg.data, 'base64').toString('utf-8');
|
||||||
|
session.ws.send(JSON.stringify({ type: 'output', data }));
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'exec_end') {
|
||||||
|
const session = _edgeExecSessions.get(msg.execId);
|
||||||
|
if (session) {
|
||||||
|
console.log('[Hawser] Exec ended:', msg.execId);
|
||||||
|
if (session.ws?.readyState === 1) { session.ws.send(JSON.stringify({ type: 'exit' })); session.ws.close(); }
|
||||||
|
_edgeExecSessions.delete(msg.execId);
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'container_event') {
|
||||||
|
const envId = _wsToEnvId.get(ws);
|
||||||
|
if (envId && msg.event) {
|
||||||
|
// Call the global handler registered by hawser.ts
|
||||||
|
if (globalThis.__hawserHandleContainerEvent) {
|
||||||
|
globalThis.__hawserHandleContainerEvent(envId, msg.event).catch((err) => {
|
||||||
|
console.error('[Hawser] Error handling container event:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'metrics') {
|
||||||
|
// Metrics from agent - save to database for dashboard graphs
|
||||||
|
const envId = _wsToEnvId.get(ws);
|
||||||
|
if (envId && msg.metrics) {
|
||||||
|
if (globalThis.__hawserHandleMetrics) {
|
||||||
|
globalThis.__hawserHandleMetrics(envId, msg.metrics).catch((err) => {
|
||||||
|
console.error('[Hawser] Error saving metrics:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose send function for hawser.ts module
|
||||||
|
globalThis.__hawserSendMessage = (envId, message) => {
|
||||||
|
const conn = _edgeConnections.get(envId);
|
||||||
|
if (!conn?.ws) return false;
|
||||||
|
try { conn.ws.send(message); return true; } catch { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============ Combined WebSocket Handler ============
|
||||||
|
const combinedWebsocket = {
|
||||||
|
async open(ws) {
|
||||||
|
const connType = ws.data?.type;
|
||||||
|
|
||||||
|
// Hawser Edge connection - wait for hello message
|
||||||
|
if (connType === 'hawser') {
|
||||||
|
console.log('[Hawser] New connection pending authentication');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal connection
|
||||||
|
const connId = 'ws-' + (++_wsConnCounter);
|
||||||
|
ws.data = ws.data || {};
|
||||||
|
ws.data.connId = connId;
|
||||||
|
const { containerId, shell, user, envId } = ws.data;
|
||||||
|
if (!containerId) { ws.send(JSON.stringify({ type: 'error', message: 'No container ID' })); ws.close(); return; }
|
||||||
|
const target = await _getDockerTarget(envId);
|
||||||
|
console.log('[WS] Open:', connId, containerId, 'target:', target.type);
|
||||||
|
|
||||||
|
// Handle Hawser Edge terminal
|
||||||
|
if (target.type === 'hawser-edge') {
|
||||||
|
const conn = _edgeConnections.get(target.environmentId);
|
||||||
|
if (!conn) { ws.send(JSON.stringify({ type: 'error', message: 'Edge agent not connected' })); ws.close(); return; }
|
||||||
|
const execId = crypto.randomUUID();
|
||||||
|
_edgeExecSessions.set(execId, { ws, execId, environmentId: target.environmentId });
|
||||||
|
ws.data.edgeExecId = execId;
|
||||||
|
conn.ws.send(JSON.stringify({ type: 'exec_start', execId, containerId, cmd: shell || '/bin/sh', user: user || 'root', cols: 120, rows: 30 }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exec = await createExec(containerId, [shell || '/bin/sh'], user || 'root', target);
|
||||||
|
const execId = exec.Id;
|
||||||
|
let dockerStream;
|
||||||
|
let headersStripped = false;
|
||||||
|
let isChunked = false;
|
||||||
|
const socketHandler = {
|
||||||
|
data(socket, data) {
|
||||||
|
if (ws.readyState === 1) {
|
||||||
|
let text = new TextDecoder().decode(data);
|
||||||
|
if (!headersStripped) {
|
||||||
|
if (text.toLowerCase().includes('transfer-encoding: chunked')) isChunked = true;
|
||||||
|
const i = text.indexOf('\\r\\n\\r\\n');
|
||||||
|
if (i > -1) { text = text.slice(i + 4); headersStripped = true; }
|
||||||
|
else if (text.startsWith('HTTP/')) return;
|
||||||
|
}
|
||||||
|
if (isChunked && text) text = text.replace(/^[0-9a-fA-F]+\\r\\n/gm, '').replace(/\\r\\n$/g, '');
|
||||||
|
if (text) ws.send(JSON.stringify({ type: 'output', data: text }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close() { if (ws.readyState === 1) { ws.send(JSON.stringify({ type: 'exit' })); ws.close(); } },
|
||||||
|
error() {},
|
||||||
|
open(socket) {
|
||||||
|
const body = JSON.stringify({ Detach: false, Tty: true });
|
||||||
|
const tokenHeader = target.type === 'tcp' && target.hawserToken ? 'X-Hawser-Token: ' + target.hawserToken + '\\r\\n' : '';
|
||||||
|
socket.write('POST /exec/' + execId + '/start HTTP/1.1\\r\\nHost: localhost\\r\\nContent-Type: application/json\\r\\n' + tokenHeader + 'Connection: Upgrade\\r\\nUpgrade: tcp\\r\\nContent-Length: ' + body.length + '\\r\\n\\r\\n' + body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (target.type === 'unix') {
|
||||||
|
dockerStream = await Bun.connect({ unix: target.socket, socket: socketHandler });
|
||||||
|
} else {
|
||||||
|
dockerStream = await Bun.connect({ hostname: target.host, port: target.port, socket: socketHandler });
|
||||||
|
}
|
||||||
|
dockerStreams.set(connId, { stream: dockerStream, execId, target });
|
||||||
|
} catch (e) { ws.send(JSON.stringify({ type: 'error', message: e.message })); ws.close(); }
|
||||||
|
},
|
||||||
|
async message(ws, message) {
|
||||||
|
const connType = ws.data?.type;
|
||||||
|
|
||||||
|
// Hawser Edge message
|
||||||
|
if (connType === 'hawser') {
|
||||||
|
try {
|
||||||
|
let msgStr = typeof message === 'string' ? message : message instanceof ArrayBuffer ? new TextDecoder().decode(message) : Buffer.isBuffer(message) ? message.toString('utf-8') : new TextDecoder().decode(new Uint8Array(message));
|
||||||
|
const msg = JSON.parse(msgStr);
|
||||||
|
await _handleHawserMessage(ws, msg);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Hawser] Error:', e.message);
|
||||||
|
ws.send(JSON.stringify({ type: 'error', error: e.message }));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge exec session input
|
||||||
|
const edgeExecId = ws.data?.edgeExecId;
|
||||||
|
if (edgeExecId) {
|
||||||
|
const session = _edgeExecSessions.get(edgeExecId);
|
||||||
|
if (session) {
|
||||||
|
const conn = _edgeConnections.get(session.environmentId);
|
||||||
|
if (conn) {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(message.toString());
|
||||||
|
if (msg.type === 'input') conn.ws.send(JSON.stringify({ type: 'exec_input', execId: edgeExecId, data: Buffer.from(msg.data).toString('base64') }));
|
||||||
|
else if (msg.type === 'resize') conn.ws.send(JSON.stringify({ type: 'exec_resize', execId: edgeExecId, cols: msg.cols, rows: msg.rows }));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal message
|
||||||
|
const connId = ws.data?.connId;
|
||||||
|
if (!connId) return;
|
||||||
|
const d = dockerStreams.get(connId);
|
||||||
|
if (!d) return;
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(message.toString());
|
||||||
|
if (msg.type === 'input' && d.stream) d.stream.write(msg.data);
|
||||||
|
else if (msg.type === 'resize' && d.execId) resizeExec(d.execId, msg.cols, msg.rows, d.target);
|
||||||
|
} catch { if (d.stream) d.stream.write(message); }
|
||||||
|
},
|
||||||
|
close(ws) {
|
||||||
|
const connType = ws.data?.type;
|
||||||
|
|
||||||
|
// Hawser Edge disconnection
|
||||||
|
if (connType === 'hawser') {
|
||||||
|
const envId = _wsToEnvId.get(ws);
|
||||||
|
if (envId) {
|
||||||
|
const conn = _edgeConnections.get(envId);
|
||||||
|
if (conn) {
|
||||||
|
console.log('[Hawser] Agent disconnected:', conn.agentId);
|
||||||
|
// Clear server-side ping interval
|
||||||
|
if (conn.pingInterval) { clearInterval(conn.pingInterval); conn.pingInterval = null; }
|
||||||
|
for (const [, p] of conn.pendingRequests) { clearTimeout(p.timeout); p.reject(new Error('Connection closed')); }
|
||||||
|
for (const [, p] of conn.pendingStreamRequests) { p.onEnd('Connection closed'); }
|
||||||
|
_edgeConnections.delete(envId);
|
||||||
|
_updateEnvStatus(envId, null);
|
||||||
|
}
|
||||||
|
_wsToEnvId.delete(ws);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge exec session close
|
||||||
|
const edgeExecId = ws.data?.edgeExecId;
|
||||||
|
if (edgeExecId) {
|
||||||
|
const session = _edgeExecSessions.get(edgeExecId);
|
||||||
|
if (session) {
|
||||||
|
const conn = _edgeConnections.get(session.environmentId);
|
||||||
|
if (conn) conn.ws.send(JSON.stringify({ type: 'exec_end', execId: edgeExecId, reason: 'user_closed' }));
|
||||||
|
_edgeExecSessions.delete(edgeExecId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal close
|
||||||
|
const connId = ws.data?.connId;
|
||||||
|
if (!connId) return;
|
||||||
|
const d = dockerStreams.get(connId);
|
||||||
|
if (d?.stream) d.stream.end();
|
||||||
|
dockerStreams.delete(connId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const insertPoint = content.indexOf('var path = env(');
|
||||||
|
if (insertPoint > -1) {
|
||||||
|
content = content.slice(0, insertPoint) + wsHandler + content.slice(insertPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
'var { fetch: handlerFetch, websocket } = getHandler();',
|
||||||
|
'var { fetch: handlerFetch, websocket: _ } = getHandler(); var websocket = combinedWebsocket;'
|
||||||
|
);
|
||||||
|
|
||||||
|
await Bun.write(indexPath, content);
|
||||||
|
console.log('✓ Patched index.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Patching build...');
|
||||||
|
await patchHandler();
|
||||||
|
await patchIndex();
|
||||||
|
console.log('✓ Done');
|
||||||
Reference in New Issue
Block a user