Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8ca246e
no message
Jan 9, 2026
bd8d8da
Update GitHub workflow to use custom GH_PAT token
Jan 12, 2026
308acff
Update GHCR authentication to use PAT as username when available
Jan 12, 2026
40b83af
Fix workflow syntax error - remove secret existence check
Jan 12, 2026
43eb8bb
Bump version to 1.0.5 and commit all pending changes
Jan 12, 2026
53495f5
Unify website UI/UX with MyPlan styling - v1.0.6
Jan 12, 2026
d3e66c5
feat: Standardize UI styling across all components
Jan 12, 2026
2e4613e
chore: Bump version to 1.0.7
Jan 12, 2026
271591b
style: Unify CSS to use design tokens and variables - Updated MyPlan,…
Jan 12, 2026
d08834f
style: Update Financial.css to use design tokens
Jan 12, 2026
e8f5795
style: Update UsageIndicator.css and LocationsManager.css to use desi…
Jan 12, 2026
ce79f2d
style: Update ServicesManager.css and EmailLogs.css to use design tokens
Jan 12, 2026
834722b
style: Update EntryForm, AdminManager, and Login CSS files to use des…
Jan 12, 2026
cf5da76
style: Replace inline styles with CSS classes in LocationsManager and…
Jan 12, 2026
e56ef1e
chore: Bump version to 1.0.8
Jan 12, 2026
c7bc4fc
style: Add purple color variables and fix invoice-controls styling
Jan 12, 2026
3f557c6
style: Center expand button + symbol in Financial tables
Jan 12, 2026
f413fd2
style: Improve Entry Form styling - Add gradient headers, subtitle, i…
Jan 12, 2026
20f63c7
style: Fix Entry Form header gradient conflict with more specific CSS…
Jan 12, 2026
7f15d99
style: Enhance Entry Form header with gradient labels and card-style …
Jan 12, 2026
45c1f0b
style: Make CLIENT NAME and SERVICE columns equal width in Entry Form
Jan 12, 2026
59e340e
style: Fix Entry Form appointments table width alignment - override c…
Jan 12, 2026
5e95984
Improve responsive design: mobile menu, navigation breakpoints, and t…
Jan 13, 2026
7c83477
Add shift-click selection and bulk mark paid/unpaid in calculator mode
Jan 14, 2026
055b34f
Improve date picker positioning and ID search functionality
Jan 16, 2026
5f9fc32
Replace SendGrid/Twilio with Resend API for email delivery
Feb 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ jobs:
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ secrets.GH_PAT || github.actor }}
password: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}

- name: Get version from package.json
id: package-version
Expand Down
62 changes: 62 additions & 0 deletions ADMIN_AUTO_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Automatic Admin User Setup

The application now **automatically creates a super admin user** when the server starts, if one doesn't already exist.

## How It Works

1. **On Server Startup:**
- Database is initialized
- Migrations run (ensures all columns exist)
- Admin user is automatically created if missing

2. **Default Credentials:**
- **Username:** `admin`
- **Password:** `admin123!`
- **Email:** (none)

3. **Customization via Environment Variables:**
You can customize the admin credentials by setting environment variables:
```bash
ADMIN_USERNAME=myadmin
ADMIN_PASSWORD=MySecurePassword123!
ADMIN_EMAIL=admin@example.com
```

## For Docker/Production

Add to your `docker-compose.yml` or environment:
```yaml
environment:
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=YourSecurePasswordHere
- ADMIN_EMAIL=admin@yourdomain.com
```

## What Gets Created

- Super admin user with `is_super_admin = 1`
- Default services (24 services) for the admin user
- All necessary database columns are automatically added

## Database Schema

The `users` table automatically includes:
- `id` (PRIMARY KEY)
- `username` (UNIQUE)
- `password_hash`
- `email`
- `is_super_admin` (INTEGER, DEFAULT 0)
- `created_at` (TIMESTAMP)

## Manual Override

If you need to manually create/update an admin user, you can still use:
```bash
node scripts/create-admin-remote.js <username> <password> [email]
```

## Security Note

⚠️ **Important:** The default password is `admin123!` - **change it immediately after first login!**

For production, always set `ADMIN_PASSWORD` environment variable to a strong password.
144 changes: 144 additions & 0 deletions MANUAL_SUPER_ADMIN_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Manual Super Admin Setup Guide

This guide shows you how to create a super admin user on a remote server using either SQL commands or the provided script.

## Option 1: Using the Script (Recommended)

The easiest way is to use the existing script:

```bash
# On your remote server
cd /path/to/HairManager
node scripts/create-super-admin.js
```

**Note:** You'll need to edit `scripts/create-super-admin.js` first to set your desired username and password (lines 16-17).

## Option 2: Manual SQL Execution

If you need to create a super admin manually via SQL, follow these steps:

### Step 1: Generate Password Hash

You need to hash your password using bcrypt. You can do this in several ways:

#### Method A: Using Node.js (on the server)
```bash
node -e "const bcrypt = require('bcrypt'); bcrypt.hash('YourPassword123!', 10).then(hash => console.log(hash));"
```

#### Method B: Using Python (if bcrypt is installed)
```python
import bcrypt
password = b'YourPassword123!'
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
print(hashed.decode())
```

#### Method C: Create a temporary script
```javascript
// hash-password.js
import bcrypt from 'bcrypt';
const password = 'YourPassword123!';
const hash = await bcrypt.hash(password, 10);
console.log(hash);
```

Run: `node hash-password.js`

### Step 2: Execute SQL

Once you have the password hash, connect to your SQLite database and run:

```sql
-- Check if user already exists
SELECT id, username, is_super_admin FROM users WHERE username = 'admin';

-- Option A: Create a NEW super admin user
INSERT INTO users (username, password_hash, email, is_super_admin)
VALUES ('admin', '$2b$10$YOUR_HASHED_PASSWORD_HERE', 'admin@example.com', 1);

-- Option B: Update an EXISTING user to be super admin
UPDATE users
SET is_super_admin = 1, password_hash = '$2b$10$YOUR_HASHED_PASSWORD_HERE'
WHERE username = 'admin';

-- Verify the user was created/updated
SELECT id, username, email, is_super_admin FROM users WHERE username = 'admin';
```

### Step 3: Verify

After creating/updating, verify with:
```sql
SELECT id, username, email, is_super_admin, created_at
FROM users
WHERE is_super_admin = 1;
```

## Option 3: Quick SQLite Command Line

If you have direct access to the database file on the server:

```bash
# Connect to the database
sqlite3 /path/to/data/hairmanager.db

# Then run the SQL commands from Option 2
```

## Database Location

The database location depends on your environment:

- **Development**: `./hairmanager.db` (in project root)
- **Production/Docker**: `./data/hairmanager.db` (in data directory)
- **Custom**: Check your `NODE_ENV` and deployment configuration

## Example: Complete Manual Setup

```bash
# 1. SSH into your server
ssh user@your-server.com

# 2. Navigate to your app directory
cd /path/to/HairManager

# 3. Generate password hash
node -e "const bcrypt = require('bcrypt'); bcrypt.hash('MySecurePassword123!', 10).then(hash => console.log(hash));"

# Copy the hash output (starts with $2b$10$...)

# 4. Connect to database
sqlite3 data/hairmanager.db

# 5. Insert super admin (replace HASH with your hash from step 3)
INSERT INTO users (username, password_hash, email, is_super_admin)
VALUES ('admin', 'HASH_FROM_STEP_3', 'admin@example.com', 1);

# 6. Verify
SELECT id, username, is_super_admin FROM users WHERE username = 'admin';

# 7. Exit
.quit
```

## Troubleshooting

### "no such table: users"
- Run the migration first: `node database/migrate.js` or ensure migrations have run

### "column is_super_admin does not exist"
- The migration should add this column automatically
- Manually add it: `ALTER TABLE users ADD COLUMN is_super_admin INTEGER DEFAULT 0;`

### "UNIQUE constraint failed"
- User already exists, use UPDATE instead of INSERT

## Security Notes

⚠️ **Important:**
- Never commit password hashes to version control
- Use strong passwords for super admin accounts
- Consider using environment variables for sensitive data
- After creating the admin, verify you can log in and then remove any temporary scripts
145 changes: 145 additions & 0 deletions database/ensureAdmin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import bcrypt from 'bcrypt';
import sqlite3 from 'sqlite3';

const runAsync = (db, sql, params = []) => {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) {
if (err) reject(err);
else resolve(this);
});
});
};

const getAsync = (db, sql, params = []) => {
return new Promise((resolve, reject) => {
db.get(sql, params, (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
};

/**
* Ensures a super admin user exists in the database
* Uses environment variables for credentials, with sensible defaults
*/
export function ensureAdminUser(dbPath) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Error opening database for admin user creation:', err);
reject(err);
return;
}
});

// Get admin credentials from environment variables or use defaults
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123!';
const adminEmail = process.env.ADMIN_EMAIL || null;

// Check if admin user already exists (case-insensitive)
getAsync(db, 'SELECT id, username, is_super_admin FROM users WHERE LOWER(username) = LOWER(?)', [adminUsername])
.then(async (existingUser) => {
if (existingUser) {
// User exists - check if they're a super admin
if (existingUser.is_super_admin === 1) {
console.log(`✓ Super admin user "${adminUsername}" already exists (ID: ${existingUser.id})`);
db.close();
resolve();
return;
} else {
// User exists but isn't super admin - upgrade them
console.log(`⚠️ User "${adminUsername}" exists but is not a super admin. Upgrading...`);
try {
const passwordHash = await bcrypt.hash(adminPassword, 10);
await runAsync(
db,
'UPDATE users SET is_super_admin = 1, password_hash = ?, email = COALESCE(?, email) WHERE username = ?',
[passwordHash, adminEmail, adminUsername]
);
console.log(`✓ User "${adminUsername}" upgraded to super admin`);
db.close();
resolve();
} catch (hashErr) {
console.error('Error hashing password:', hashErr);
db.close();
reject(hashErr);
}
return;
}
}

// User doesn't exist - create new super admin
console.log(`📝 Creating super admin user "${adminUsername}"...`);
try {
const passwordHash = await bcrypt.hash(adminPassword, 10);

const result = await runAsync(
db,
'INSERT INTO users (username, password_hash, email, is_super_admin) VALUES (?, ?, ?, ?)',
[adminUsername, passwordHash, adminEmail, 1]
);

const userId = result.lastID;
console.log(`✓ Super admin user "${adminUsername}" created (ID: ${userId})`);

// Create default services for the new admin user
const defaultServices = [
{ service_name: 'Blow Dry', type: 'Hair', price: 15.00 },
{ service_name: 'Shampoo & Set', type: 'Hair', price: 14.00 },
{ service_name: 'Dry Cut', type: 'Hair', price: 14.00 },
{ service_name: 'Cut & Blow Dry', type: 'Hair', price: 25.00 },
{ service_name: 'Cut & Set', type: 'Hair', price: 24.00 },
{ service_name: 'Restyling', type: 'Hair', price: 30.00 },
{ service_name: 'Gents Dry Cut', type: 'Hair', price: 14.50 },
{ service_name: 'Clipper Cuts', type: 'Hair', price: 6.00 },
{ service_name: 'Beard Trim', type: 'Hair', price: 5.00 },
{ service_name: 'Child Cut', type: 'Hair', price: 10.00 },
{ service_name: 'Child Cut & Blow Dry', type: 'Hair', price: 18.00 },
{ service_name: 'Other', type: 'Hair', price: 0.00 },
{ service_name: 'File & Polish', type: 'Nails', price: 10.00 },
{ service_name: 'Manicure', type: 'Nails', price: 18.00 },
{ service_name: 'Gel Polish', type: 'Nails', price: 20.00 },
{ service_name: 'Removal', type: 'Nails', price: 6.00 },
{ service_name: 'Gel Removal & Re-Apply', type: 'Nails', price: 25.00 },
{ service_name: 'Pedicure', type: 'Nails', price: 20.00 },
{ service_name: 'Blow Dry & Fringe Trim', type: 'Hair', price: 17.00 },
{ service_name: 'Nails Cut & Filed', type: 'Nails', price: 6.00 },
{ service_name: 'Wash & Cut', type: 'Hair', price: 20.00 },
{ service_name: 'Colour', type: 'Hair', price: 60.00 },
{ service_name: 'Colour, cut & blow dry', type: 'Hair', price: 45.00 },
{ service_name: 'Hair wash', type: 'Hair', price: 5.00 }
];

// Insert default services for the new admin user
for (const service of defaultServices) {
await runAsync(db, 'INSERT OR IGNORE INTO services (user_id, service_name, type, price) VALUES (?, ?, ?, ?)',
[userId, service.service_name, service.type, service.price]);
}

console.log(`✓ Default services created for super admin user`);
console.log(`\n🔑 Admin Credentials:`);
console.log(` Username: ${adminUsername}`);
console.log(` Password: ${adminPassword}`);
if (adminEmail) {
console.log(` Email: ${adminEmail}`);
}
console.log(`\n⚠️ IMPORTANT: Change the default password after first login!`);
console.log(` You can set ADMIN_USERNAME, ADMIN_PASSWORD, and ADMIN_EMAIL environment variables to customize.\n`);

db.close();
resolve();
} catch (error) {
console.error('Error creating super admin:', error);
db.close();
reject(error);
}
})
.catch((err) => {
console.error('Error checking for admin user:', err);
db.close();
reject(err);
});
});
}
1 change: 1 addition & 0 deletions database/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function initDatabase(dbPath) {
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
email TEXT,
is_super_admin INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`)
Expand Down
Loading