# Visions Salon & Spa — Booking System

## Visions Salon & Spa — Booking System

A self-hosted appointment booking site for PCTI Visions Salon & Spa. Users sign in with Google, Microsoft, or email+password, pick a service and time slot, and reserve it. Double-booking is prevented at the database level.

* **Stack:** Node.js 20+, Express, SQLite (via `better-sqlite3`), Passport.js
* **Auth:** Google OAuth 2.0, Microsoft OAuth 2.0, and email+password. You can enable any subset — missing OAuth credentials just hide those buttons.
* **Passwords:** hashed with Node's built-in `crypto.scrypt` (no native deps)
* **Storage:** A single `visions.db` file in the project folder
* **Admin view:** Anyone whose email is listed in `ADMIN_EMAILS` can visit `/admin`

***

### Table of contents

1. Local quick start
2. Set up Google OAuth *(optional)*
3. Set up Microsoft OAuth *(optional)*
4. Deploy to a Linux server
   * 4.1 Server prep (Ubuntu 22.04 / 24.04)
   * 4.2 Install the app
   * 4.3 Run it as a service with systemd
   * 4.4 Put nginx in front of it
   * 4.5 Add HTTPS with Let's Encrypt
   * 4.6 Update the OAuth redirect URLs
5. Operations
6. Customizing services and time slots
7. Troubleshooting

> **Email + password is always available** — you don't need to set up either OAuth provider if you don't want to. The login page will adapt to show whatever is configured.

***

### 1. Local quick start

```bash
# Install Node 18+ if you don't have it
node --version    # should print v18.x or higher

# Install dependencies
npm install

# Create your config
cp .env.example .env
# then edit .env (see sections 2 & 3 for OAuth credentials)

# Generate a strong session secret and paste it into .env
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

# Run it
npm start
```

Visit <http://localhost:3000>.

For **local** testing, set `BASE_URL=http://localhost:3000` in `.env` and register that as the OAuth callback origin — see the next two sections.

***

### 2. Set up Google OAuth

1. Go to <https://console.cloud.google.com/>.
2. Create a new project (e.g. `visions-salon`) or pick an existing one.
3. In the left menu: **APIs & Services → OAuth consent screen**
   * User type: **External**
   * App name: `Visions Salon & Spa`
   * User support email: your email
   * Add your email under Developer contact
   * Scopes: `.../auth/userinfo.email` and `.../auth/userinfo.profile`
   * Save. You can leave it in "Testing" mode and add yourself as a test user, or publish it if you want anyone to sign in.
4. In the left menu: **APIs & Services → Credentials → Create Credentials → OAuth client ID**
   * Application type: **Web application**
   * Name: `Visions Salon Web`
   * **Authorized JavaScript origins:**
     * `http://localhost:3000` (for local testing)
     * `https://your.domain.com` (for production)
   * **Authorized redirect URIs:**
     * `http://localhost:3000/auth/google/callback`
     * `https://your.domain.com/auth/google/callback`
   * Create. Copy the **Client ID** and **Client secret** into your `.env`:

     ```
     GOOGLE_CLIENT_ID=...
     GOOGLE_CLIENT_SECRET=...
     ```

***

### 3. Set up Microsoft OAuth

1. Go to <https://portal.azure.com> → search for **Microsoft Entra ID** (formerly Azure AD) → **App registrations → New registration**.
2. Fill in:
   * **Name:** `Visions Salon & Spa`
   * **Supported account types:** *Accounts in any organizational directory and personal Microsoft accounts* (this is the "common" tenant and is usually what you want for a school app — it accepts school, work, and personal Microsoft accounts).
   * **Redirect URI:** platform **Web**, value `https://your.domain.com/auth/microsoft/callback` (for local dev, use `http://localhost:3000/auth/microsoft/callback` — you can add additional URIs after creation under **Authentication**).
3. Click **Register**. Copy the **Application (client) ID** into `.env` as `MICROSOFT_CLIENT_ID`.
4. In the left menu of the app: **Certificates & secrets → Client secrets → New client secret**.
   * Description: `visions-salon`
   * Expiry: 24 months
   * Copy the **Value** (not the Secret ID) immediately into `.env` as `MICROSOFT_CLIENT_SECRET`. You won't be able to see it again later.
5. In the left menu: **API permissions → Add a permission → Microsoft Graph → Delegated permissions →** check `User.Read` (usually already there), then click **Grant admin consent** if you're an admin. `email`, `profile`, `openid` are standard OpenID scopes and may auto-include.
6. In `.env` leave `MICROSOFT_TENANT=common` unless you want to lock it to a single tenant.

***

### 4. Deploy to a Linux server

These instructions target Ubuntu 22.04 or 24.04. They work with only small tweaks on Debian, Rocky, Alma, etc.

#### 4.1 Server prep (Ubuntu 22.04 / 24.04)

SSH into your server as a sudo user, then:

```bash
# Keep the system patched
sudo apt update && sudo apt upgrade -y

# Build tools (better-sqlite3 compiles a native module)
sudo apt install -y build-essential python3 git nginx ufw

# Install Node.js 20 LTS (NodeSource)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node --version   # v20.x
npm --version

# Firewall: allow SSH, HTTP, HTTPS
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable

# Create a dedicated, unprivileged user for the app
sudo useradd --system --create-home --shell /bin/bash visions
```

#### 4.2 Install the app

```bash
# Switch to the app user
sudo -iu visions

# Copy the project to /home/visions/app (several ways to do this):
# Option A: rsync from your laptop
#   rsync -av --exclude node_modules --exclude .env --exclude '*.db' \
#     ./visions-salon/ visions@your.server:/home/visions/app/
# Option B: clone from a git repo
#   git clone https://github.com/you/visions-salon.git app
# Option C: scp a zip and unzip

cd ~/app
npm ci --omit=dev        # production install, reads package-lock.json

# Create production env
cp .env.example .env
nano .env
```

Fill in `.env` with **production** values:

```
PORT=3000
NODE_ENV=production
BASE_URL=https://your.domain.com

SESSION_SECRET=<paste output of: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))">

GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...

MICROSOFT_CLIENT_ID=...
MICROSOFT_CLIENT_SECRET=...
MICROSOFT_TENANT=common

ADMIN_EMAILS=mcheng@kean.edu
```

Verify it boots:

```bash
node server.js
# should print: Visions Salon & Spa listening on http://localhost:3000
# Ctrl+C to stop
exit   # leave the visions user shell
```

#### 4.3 Run it as a service with systemd

Create `/etc/systemd/system/visions.service`:

```bash
sudo nano /etc/systemd/system/visions.service
```

Paste:

```ini
[Unit]
Description=Visions Salon & Spa booking
After=network.target

[Service]
Type=simple
User=visions
Group=visions
WorkingDirectory=/home/visions/app
EnvironmentFile=/home/visions/app/.env
ExecStart=/usr/bin/node /home/visions/app/server.js
Restart=on-failure
RestartSec=5

# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/visions/app
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true

[Install]
WantedBy=multi-user.target
```

Then:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now visions
sudo systemctl status visions
# logs:
sudo journalctl -u visions -f
```

The app is now running on `127.0.0.1:3000` and will restart on crash or reboot.

#### 4.4 Put nginx in front of it

Create `/etc/nginx/sites-available/visions`:

```bash
sudo nano /etc/nginx/sites-available/visions
```

Paste (replace `your.domain.com`):

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name your.domain.com;

    # Let certbot handle redirect-to-HTTPS after we add the cert
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";
    }

    # Small tweaks
    client_max_body_size 2m;
    access_log /var/log/nginx/visions.access.log;
    error_log  /var/log/nginx/visions.error.log;
}
```

Enable it:

```bash
sudo ln -s /etc/nginx/sites-available/visions /etc/nginx/sites-enabled/visions
sudo rm -f /etc/nginx/sites-enabled/default   # optional, removes the welcome page
sudo nginx -t
sudo systemctl reload nginx
```

Point your domain's **A record** at the server's public IP and wait for DNS to propagate, then visit `http://your.domain.com` — you should see the site (over plain HTTP for now).

#### 4.5 Add HTTPS with Let's Encrypt

```bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your.domain.com
# pick: redirect HTTP → HTTPS when it asks
```

Certbot will edit the nginx config to add the certificate and the HTTP→HTTPS redirect. The app's session cookies are set with `secure: true` when `NODE_ENV=production`, so HTTPS is required for login to work correctly. Auto-renewal is set up by the certbot package (`systemctl list-timers | grep certbot`).

#### 4.6 Update the OAuth redirect URLs

Now that your site is live at `https://your.domain.com`, go back and:

* In **Google Cloud Console** → Credentials → your OAuth client, make sure `https://your.domain.com/auth/google/callback` is listed under **Authorized redirect URIs**.
* In **Azure Portal** → your App registration → Authentication, make sure `https://your.domain.com/auth/microsoft/callback` is listed as a Web redirect URI.

If `BASE_URL` in `.env` ever changes, restart the service:

```bash
sudo systemctl restart visions
```

***

### 5. Operations

**View logs**

```bash
sudo journalctl -u visions -f
sudo tail -f /var/log/nginx/visions.access.log
```

**Restart after changing `.env`**

```bash
sudo systemctl restart visions
```

**Deploy an update**

```bash
sudo -iu visions
cd ~/app
# pull or rsync the new code here
npm ci --omit=dev
exit
sudo systemctl restart visions
```

**Back up the database**

The whole app state is in a few SQLite files. A cron backup:

```bash
sudo crontab -e
# daily at 2am
0 2 * * * sqlite3 /home/visions/app/visions.db ".backup '/var/backups/visions-$(date +\%F).db'"
```

**Restore**: stop the service, replace `visions.db`, start the service.

**Inspect bookings from the CLI**

```bash
sudo -u visions sqlite3 /home/visions/app/visions.db \
  "SELECT date, time_slot, service, email FROM bookings b
   JOIN users u ON u.id = b.user_id
   WHERE status='confirmed' ORDER BY date, time_slot;"
```

**Reset a user's forgotten password**

There is no self-service "forgot password" flow (it would require SMTP, which this app deliberately avoids). To reset a password as an admin:

```bash
# 1. Generate a new hash in the project directory
sudo -iu visions
cd ~/app
node -e "console.log(require('./config/password').hashPassword(process.argv[1]))" 'NewTempPassword123'
# Copy the resulting hash string (starts with scrypt$...)
exit

# 2. Write it into the database
sudo -u visions sqlite3 /home/visions/app/visions.db \
  "UPDATE users SET password_hash = 'scrypt$N=32768,r=8,p=1$...' \
   WHERE email = 'user@example.com' AND provider = 'email';"
```

Give the user the temporary password and tell them to change it immediately via `/change-password.html`.

**Restrict who can sign up**

Set `ALLOWED_SIGNUP_DOMAINS` in `.env` to a comma-separated list of email domains. If set, only email addresses from those domains can create accounts. Leave it blank to allow anyone.

```
ALLOWED_SIGNUP_DOMAINS=pcti.org,kean.edu
```

This only affects email+password signup — OAuth users from Google or Microsoft are never blocked (the identity provider controls that side).

***

### 6. Customizing services and time slots

Both are defined in **one place**: `routes/api.js`, at the top:

```js
const SERVICES = [
  { id: 'haircut_blowdry',  name: 'Haircut + Blow Dry',        price: 12 },
  // ...
];

const TIME_SLOTS = [
  { value: '08:30', label: '8:30 AM' },
  // ...
];
```

Edit and `sudo systemctl restart visions`. No database migration needed — bookings just store the service *name* as a string and the time as `HH:MM`.

The salon-is-closed rule (weekends) is in the same file:

```js
function isOpenDay(dateStr) { ... }
```

***

### 7. Troubleshooting

**"redirect\_uri\_mismatch" after clicking Google or Microsoft sign-in** The URL in the provider console must match *exactly* — scheme, host, path, and no trailing slash. Check `BASE_URL` in `.env`.

**Login succeeds but immediately logs out / looping redirects** Almost always a cookie issue. In production with HTTPS, ensure:

* `NODE_ENV=production` is set
* The site is actually reached over `https://` (not `http://`)
* `app.set('trust proxy', 1)` is active (it is by default in `server.js`)
* Nginx is forwarding `X-Forwarded-Proto` (the snippet above does)

**`better-sqlite3` fails to install on the server** You need a C toolchain. On Ubuntu: `sudo apt install -y build-essential python3`.

**Port 3000 already in use** Change `PORT=3000` in `.env` and update the `proxy_pass` in nginx to match, then restart both.

**Admin page shows "Forbidden"** Your signed-in email isn't in `ADMIN_EMAILS`. Edit `.env`, then `sudo systemctl restart visions`. The match is case-insensitive.

**I want to wipe the database and start fresh**

```bash
sudo systemctl stop visions
sudo rm /home/visions/app/visions.db
sudo systemctl start visions
```

***

### Security notes

* Sessions are HTTP-only, `SameSite=Lax`, `Secure` in production.
* The session cookie secret is stored in `.env` — keep it out of git.
* **Passwords** are hashed with scrypt (N=2^15, r=8, p=1) using Node's built-in `crypto` module. Hashes include the salt and work parameters, so you can raise the difficulty later without invalidating existing passwords. Verification is constant-time to resist timing attacks.
* **Rate limiting** on auth endpoints: 10 login attempts per IP per 15min, 5 per email per 15min, 5 signups per IP per hour. Values are in `routes/auth.js` if you want to adjust them.
* **Account enumeration**: login failures return the same error whether the email exists or not.
* Double-booking is prevented by a `UNIQUE` index on `(date, time_slot)` scoped to `status='confirmed'`, so two simultaneous POSTs cannot both succeed — one will get a 409 and the frontend reloads availability.
* CSP is set via Helmet; inline scripts are blocked. If you add a third-party script, update the `scriptSrc` list in `server.js`.
* The admin allowlist is email-based. If you want stronger control, change `middleware/auth.js` to also require a specific provider, e.g. only allow admin access when `req.user.provider === 'microsoft'` and the tenant matches your organization.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://www.csprinciples.com/visions-salon-and-spa-booking-system.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
