Add files via upload

This commit is contained in:
jasonMPM
2026-03-03 16:23:30 -06:00
committed by GitHub
parent c1b3e01b74
commit e53b0c4bb2
9 changed files with 178 additions and 235 deletions

View File

@@ -3,7 +3,7 @@ FROM node:20-alpine
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci --omit=dev RUN npm install --omit=dev
COPY . . COPY . .

304
README.md
View File

@@ -17,14 +17,27 @@ Runs as a single container — designed for Unraid but works on any Docker host.
--- ---
## Logo & Image Hosting
All images referenced in the signature template must be publicly accessible via HTTPS.
The default logo URL is:
```
https://alwisp.com/uploads/logo.png
```
Upload your logo and any other signature images to `https://alwisp.com/uploads/` and
reference them by full URL in the template. The logo URL can also be changed live
in the Template Editor UI without rebuilding the container.
---
## Environment Variables ## Environment Variables
All secrets and configuration are passed as **environment variables at runtime**. All secrets and configuration are passed as **environment variables at runtime**.
No `.env` file is committed to this repo — see `.env.example` for all variable names and descriptions. No `.env` file is committed to this repo — see `.env.example` for all variable names.
| Variable | Description | Default | | Variable | Description | Default |
|---|---|---| |---|---|---|
| `GOOGLE_ADMIN_EMAIL` | Workspace admin email for Directory API | *(required)* | | `GOOGLE_ADMIN_EMAIL` | Workspace admin email (e.g. jason@messagepoint.tv) | *(required)* |
| `GOOGLE_CUSTOMER_ID` | Use `my_customer` for primary domain | `my_customer` | | `GOOGLE_CUSTOMER_ID` | Use `my_customer` for primary domain | `my_customer` |
| `SERVICE_ACCOUNT_KEY_PATH` | Path to sa.json inside container | `/app/secrets/sa.json` | | `SERVICE_ACCOUNT_KEY_PATH` | Path to sa.json inside container | `/app/secrets/sa.json` |
| `ADMIN_USERNAME` | Web UI login username | *(required)* | | `ADMIN_USERNAME` | Web UI login username | *(required)* |
@@ -34,12 +47,12 @@ No `.env` file is committed to this repo — see `.env.example` for all variable
| `NODE_ENV` | Node environment | `production` | | `NODE_ENV` | Node environment | `production` |
### On Unraid ### On Unraid
Set each variable directly in the **Docker container template UI** under the Set each variable directly in the **Docker container template UI** under Variables.
"Variables" section. No file needed on the host.
### For Local Development ### For Local Development
Copy `.env.example` to `.env`, fill in your values, then run:
```bash ```bash
cp .env.example .env
# fill in values
docker-compose --env-file .env up -d --build docker-compose --env-file .env up -d --build
``` ```
@@ -47,192 +60,126 @@ docker-compose --env-file .env up -d --build
## Part 1 — Google Cloud Console Setup ## Part 1 — Google Cloud Console Setup
This section walks through creating the service account and downloading the key
that allows the app to impersonate users in your Google Workspace domain.
### Step 1 — Create a Google Cloud Project ### Step 1 — Create a Google Cloud Project
1. Open https://console.cloud.google.com and sign in with your Google Workspace admin account 1. Open https://console.cloud.google.com sign in with your Google Workspace admin account
2. Click the **project dropdown** in the top navigation bar (next to the Google Cloud logo) 2. Click the **project dropdown** (top nav bar) → **New Project**
3. Click **New Project** in the top right of the dialog 3. Name: `email-sig-manager`**Create**
4. Enter a project name: `email-sig-manager` 4. Select the new project from the dropdown to make it active
5. Leave the organization as your Workspace domain → click **Create**
6. Wait a few seconds, then select the new project from the dropdown to make it active
> **Note:** If your org already has a GCP project you prefer to use, you can skip
> project creation and just enable the APIs inside the existing project.
---
### Step 2 — Enable Required APIs ### Step 2 — Enable Required APIs
The app needs two Google APIs enabled in your project. **Admin SDK API:**
1. Go to **APIs & Services → Library**
2. Search `Admin SDK API` → click result → **Enable**
**Enable Admin SDK API:** **Gmail API:**
1. In the GCP Console, go to **APIs & Services → Library** (left sidebar)
2. Search for `Admin SDK API`
3. Click the result → click **Enable**
**Enable Gmail API:**
1. Go back to **APIs & Services → Library** 1. Go back to **APIs & Services → Library**
2. Search for `Gmail API` 2. Search `Gmail API` → click result → **Enable**
3. Click the result → click **Enable**
Both should now show as **Enabled** under **APIs & Services → Enabled APIs & Services**.
---
### Step 3 — Create the Service Account ### Step 3 — Create the Service Account
1. In the left sidebar, go to **IAM & Admin → Service Accounts** 1. Go to **IAM & Admin → Service Accounts**
2. Click **+ Create Service Account** at the top 2. Click **+ Create Service Account**
3. Fill in the details: 3. Name: `email-sig-manager`**Create and Continue**
- **Service account name:** `email-sig-manager` 4. Skip role assignment → **Continue** → skip user access → **Done**
- **Service account ID:** will auto-fill as `email-sig-manager`
- **Description:** `Email signature push service for Google Workspace`
4. Click **Create and Continue**
5. On the "Grant this service account access to project" step — **skip this, click Continue**
6. On the "Grant users access" step — **skip this too, click Done**
You will land back on the service accounts list and see your new account listed. ### Step 4 — Enable Domain-Wide Delegation
--- 1. Click the service account name to open it
2. Click **Edit** (pencil icon)
### Step 4 — Enable Domain-Wide Delegation on the Service Account 3. Expand **Show advanced settings**
4. Check **Enable Google Workspace Domain-wide Delegation**
1. Click the service account name (`email-sig-manager`) in the list to open it 5. Enter product name: `Email Signature Manager`
2. Click the **Edit** button (pencil icon) at the top
3. Scroll down to find **"Show advanced settings"** — click it to expand
4. Check the box for **"Enable Google Workspace Domain-wide Delegation"**
5. Enter a product name for the consent screen if prompted (e.g., `Email Signature Manager`)
6. Click **Save** 6. Click **Save**
7. Back on the service account detail page, note the **Client ID** (long number) — copy it
After saving, return to the service account detail page. You should now see a ### Step 5 — Download the JSON Key
**"Domain-wide delegation"** line showing a **Client ID** (a long numeric string,
e.g., `112233445566778899`). **Copy this Client ID** — you will need it in Part 2.
--- 1. Click the **Keys** tab on the service account
2. **Add Key → Create New Key → JSON → Create**
### Step 5 — Download the Service Account JSON Key 3. Rename the downloaded file to `sa.json`
4. Place it at `secrets/sa.json` on your Unraid host — never commit to GitHub
1. On the service account detail page, click the **Keys** tab
2. Click **Add Key → Create New Key**
3. Select **JSON** as the key type → click **Create**
4. The key file downloads automatically to your computer
5. **Rename the file to `sa.json`**
6. Store it securely — this file grants impersonation access to all users in your domain
> This file goes in the `secrets/` folder on your Unraid host. It is gitignored
> and must never be committed to GitHub.
--- ---
## Part 2 — Google Admin Console Authorization ## Part 2 — Google Admin Console Authorization
This section delegates authority to the service account inside Google Admin, ### Step 6 — Authorize Domain-Wide Delegation
allowing it to impersonate users and update their Gmail signatures.
### Step 6 — Authorize the Service Account in Google Admin
1. Open a new tab and go to https://admin.google.com
2. Sign in with your **Google Workspace super admin** account
3. In the left sidebar, navigate to:
**Security → Access and data control → API controls**
4. On the API controls page, click **Manage Domain Wide Delegation**
5. Click **Add new** to add a new authorized client
Fill in the form:
- **Client ID:** Paste the numeric Client ID you copied in Step 4
(find it again at GCP Console → IAM & Admin → Service Accounts → your account → Details)
- **OAuth Scopes:** Paste the following exactly, as one comma-separated line:
```
https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/gmail.settings.basic
```
1. Go to https://admin.google.com — sign in as super admin
2. Navigate to: **Security → Access and data control → API controls**
3. Click **Manage Domain Wide Delegation**
4. Click **Add new**
5. Fill in:
- **Client ID:** *(paste the number from Step 4)*
- **OAuth Scopes:**
```
https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/gmail.settings.basic
```
6. Click **Authorize** 6. Click **Authorize**
> If you receive an error saying "client ID not found", wait 23 minutes after > If you get "client ID not found" wait 23 minutes after Step 4 and try again.
> enabling domain-wide delegation in Step 4 and try again — GCP propagation
> can take a moment.
--- ### Step 7 — Verify
### Step 7 — Verify the Authorization 1. Click **View details** next to your new entry
2. Confirm both scopes are listed
1. After clicking Authorize you should see the new entry in the list 3. If a scope is missing, click Edit, re-enter the full scope string, and re-authorize
2. Click **View details** next to your entry
3. Confirm both scopes are listed:
- `https://www.googleapis.com/auth/admin.directory.user.readonly`
- `https://www.googleapis.com/auth/gmail.settings.basic`
4. If a scope is missing, click **Edit**, re-enter the full comma-separated scope string, and click **Authorize** again
> **Multi-party approval note:** If your Google Workspace org has multi-party
> approval enabled for admin actions, another super admin will need to approve
> this delegation before it takes effect.
--- ---
## Part 3 — Unraid Deployment ## Part 3 — Unraid Deployment
### Step 8 — Clone the Repository ### Step 8 — Clone the Repo
SSH into your Unraid server or use the Unraid terminal:
```bash ```bash
cd /mnt/user/appdata cd /mnt/user/appdata
git clone https://github.com/YOURUSERNAME/email-sig-manager.git git clone https://github.com/YOURUSERNAME/email-sig-manager.git email-sigs
cd email-sig-manager cd email-sigs
``` ```
### Step 9 — Place the Service Account Key ### Step 9 — Place the Service Account Key
Copy `sa.json` (downloaded in Step 5) into the `secrets/` folder:
```bash ```bash
# From your PC to Unraid (run this on your PC): # From your PC (run on your PC):
scp sa.json root@UNRAID-IP:/mnt/user/appdata/email-sig-manager/secrets/sa.json scp sa.json root@UNRAID-IP:/mnt/user/appdata/email-sigs/secrets/sa.json
```
Or copy via SMB: `\\UNRAID-IP\appdata\email-sigs\secrets\`
### Step 10 — Upload Logo
Upload your logo to `https://alwisp.com/uploads/logo.png` via your web server.
The template references this URL directly — no local file needed.
### Step 11 — Build the Docker Image
SSH into Unraid and run:
```bash
cd /mnt/user/appdata/email-sigs
docker build -t email-sigs:latest .
``` ```
Or copy it via your Unraid SMB share using Windows Explorer: ### Step 12 — Configure Container in Unraid Docker UI
```
\\UNRAID-IP\appdata\email-sig-manager\secrets\
```
### Step 10 — Add Your Company Logo - **Name:** `email-sigs`
- **Repository:** `email-sigs` *(local image built in Step 11)*
Place your logo file at: **Volume mappings:**
```
/mnt/user/appdata/email-sig-manager/public/assets/logo.png
```
Recommended: PNG, transparent background, 160×160px or smaller.
### Step 11 — Configure Container in Unraid Docker UI | Host Path | Container Path |
1. In the Unraid web UI, go to **Docker → Add Container**
2. Set the following:
- **Name:** `email-sig-manager`
- **Repository:** *(leave blank if building locally — use the path method below)*
- **Network Type:** Bridge
**Add these Volume mappings:**
| Container Path | Host Path |
|---|---| |---|---|
| `/app/secrets` | `/mnt/user/appdata/email-sig-manager/secrets` | | `/mnt/user/appdata/email-sigs/secrets` | `/app/secrets` |
| `/app/data` | `/mnt/user/appdata/email-sig-manager/data` | | `/mnt/user/appdata/email-sigs/data` | `/app/data` |
| `/app/public/assets` | `/mnt/user/appdata/email-sig-manager/public/assets` | | `/mnt/user/appdata/email-sigs/public/assets` | `/app/public/assets` |
**Add these Port mapping:** **Port mapping:** Host `3000` → Container `3000`
| Container Port | Host Port | **Variables:**
|---|---|
| `3000` | `3000` *(or any open port)* |
**Add these Variables** (click Add Variable for each):
| Name | Value | | Name | Value |
|---|---| |---|---|
| `GOOGLE_ADMIN_EMAIL` | `admin@messagepointmedia.com` | | `GOOGLE_ADMIN_EMAIL` | `jason@messagepoint.tv` |
| `GOOGLE_CUSTOMER_ID` | `my_customer` | | `GOOGLE_CUSTOMER_ID` | `my_customer` |
| `SERVICE_ACCOUNT_KEY_PATH` | `/app/secrets/sa.json` | | `SERVICE_ACCOUNT_KEY_PATH` | `/app/secrets/sa.json` |
| `ADMIN_USERNAME` | `admin` | | `ADMIN_USERNAME` | `admin` |
@@ -241,57 +188,39 @@ Recommended: PNG, transparent background, 160×160px or smaller.
| `CRON_SCHEDULE` | `0 2 * * *` | | `CRON_SCHEDULE` | `0 2 * * *` |
| `NODE_ENV` | `production` | | `NODE_ENV` | `production` |
### Step 12 — Build and Start
```bash
cd /mnt/user/appdata/email-sig-manager
docker-compose up -d --build
```
--- ---
## Part 4 — First Run & Verification ## Part 4 — First Run & Verification
### Step 13 — Access the Admin UI ### Step 13 — Access the UI
Open a browser and navigate to:
``` ```
http://UNRAID-IP:3000 http://UNRAID-IP:3000
``` ```
Log in with your `ADMIN_USERNAME` and `ADMIN_PASSWORD`.
### Step 14 — Test with a Single User First ### Step 14 — Preview and Test
1. Go to the **Template Editor** page 1. Go to **Template Editor** — verify the preview renders correctly
2. Verify the live preview renders correctly with your info 2. Back on **Dashboard**enter your own email → **Push Single User**
3. Make any template adjustments needed 3. Open Gmail → compose a new message → verify signature appears
4. Return to the **Dashboard** 4. Check Gmail mobile app (force-close and reopen if needed)
5. In the **Push Single User** field, enter your own email address
6. Click **Push Single User**
7. Open Gmail in a new tab → compose a new email → verify the signature appears correctly
8. Check the Gmail mobile app as well (force-close and reopen if needed)
### Step 15 — Push to All Users ### Step 15 — Push to All Users
Once you've verified your own signature looks correct: Once verified, click **Push to All Users** on the Dashboard.
The nightly cron will keep all signatures in sync automatically.
1. Click **Push to All Users** on the Dashboard
2. Confirm the dialog
3. Watch the audit log populate with success/error statuses
4. The nightly cron will take over automatically from here
--- ---
## Updating the Project ## Updating
```bash ```bash
cd /mnt/user/appdata/email-sig-manager cd /mnt/user/appdata/email-sigs
git pull git pull
docker-compose up -d --build docker build -t email-sigs:latest .
docker restart email-sigs
``` ```
`secrets/`, `data/`, and host-managed files are gitignored and unaffected by pulls.
--- ---
## Project Structure ## Project Structure
@@ -299,7 +228,7 @@ docker-compose up -d --build
``` ```
email-sig-manager/ email-sig-manager/
├── src/ ├── src/
│ ├── index.js # Express app entry │ ├── index.js # Express app (no dotenv — vars injected by Docker)
│ ├── routes/ │ ├── routes/
│ │ ├── admin.js # Template, logs, users API │ │ ├── admin.js # Template, logs, users API
│ │ └── push.js # Signature push logic + batch runner │ │ └── push.js # Signature push logic + batch runner
@@ -316,11 +245,10 @@ email-sig-manager/
├── public/ ├── public/
│ ├── dashboard.html # Admin dashboard UI │ ├── dashboard.html # Admin dashboard UI
│ ├── editor.html # Template editor with live preview │ ├── editor.html # Template editor with live preview
│ └── assets/ │ └── assets/ # Local asset dir (optional, logo served remotely)
│ └── logo.png # Company logo (add your own)
├── secrets/ # Gitignored — sa.json goes here ├── secrets/ # Gitignored — sa.json goes here
├── data/ # Gitignored — SQLite DB lives here ├── data/ # Gitignored — SQLite DB lives here
├── Dockerfile ├── Dockerfile # Uses npm install (not npm ci)
├── docker-compose.yml ├── docker-compose.yml
├── package.json ├── package.json
└── .env.example # Variable reference — safe to commit └── .env.example # Variable reference — safe to commit
@@ -341,29 +269,33 @@ email-sig-manager/
## Troubleshooting ## Troubleshooting
**"Cannot find module" errors on startup**
Rebuild the image: `docker build -t email-sigs:latest .` then restart.
**Container won't start** **Container won't start**
Run `docker logs email-sig-manager`. Most likely a missing env variable or malformed `sa.json`. Run `docker logs email-sigs` — most likely a missing env variable or malformed `sa.json`.
**"No primary sendAs" error** **"No primary sendAs" error**
The user may not have an active Gmail account, or domain-wide delegation scopes were not saved correctly in Google Admin. The user may not have an active Gmail account, or domain-wide delegation scopes were not saved correctly.
**"Client ID not found" when authorizing in Google Admin** **"Client ID not found" in Google Admin**
Wait 23 minutes after enabling domain-wide delegation in GCP and try again. Wait 23 minutes after enabling domain-wide delegation in GCP, then try again.
**Signatures not showing on mobile** **Signatures not showing on mobile**
Gmail iOS/Android automatically uses the web signature. Have the user force-close and reopen the Gmail app after the push. Gmail iOS/Android uses the web signature automatically. Force-close and reopen the app.
**Template changes not saving** **Template changes not saving**
Verify the container has write access to the `templates/` directory. A `.bak` file is created on every save. Verify the container has write access to `templates/`. A `.bak` file is created on every save.
**401 / permission denied errors in logs** **401 / permission denied errors**
The OAuth scopes in Google Admin may not have saved correctly. Go back to Admin Console → Security → API Controls → Domain Wide Delegation → View Details and verify both scopes are listed. Go to Google Admin → Security → API Controls → Domain Wide Delegation → View Details
and verify both OAuth scopes are listed correctly.
--- ---
## Security Notes ## Security Notes
- `sa.json` grants impersonation rights to every user in your domain — treat it like a master key - `sa.json` grants impersonation rights to every user in your domain — treat it like a master key
- Never commit `sa.json` or a populated `.env` to GitHub - Never commit `sa.json` to GitHub — it is gitignored
- Change `ADMIN_PASSWORD` before first deployment - Change `ADMIN_PASSWORD` before first deployment
- Consider placing the UI behind HTTPS if accessible outside your LAN - Consider HTTPS via a reverse proxy if the UI is accessible outside your LAN

View File

@@ -1,10 +1,6 @@
version: "3.8" version: "3.8"
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Email Signature Manager
#
# Environment variables are passed in at runtime — not stored in this file.
#
# On Unraid: Set each variable in the Docker container template UI # On Unraid: Set each variable in the Docker container template UI
# For local dev: Create a .env file (see .env.example) and run: # For local dev: Create a .env file (see .env.example) and run:
# docker-compose --env-file .env up -d --build # docker-compose --env-file .env up -d --build

View File

@@ -29,9 +29,9 @@
.badge.success { background: #1a4a1a; color: #4caf50; } .badge.success { background: #1a4a1a; color: #4caf50; }
.badge.error { background: #4a1a1a; color: #f44336; } .badge.error { background: #4a1a1a; color: #f44336; }
.badge.skipped { background: #3a3a1a; color: #aaa; } .badge.skipped { background: #3a3a1a; color: #aaa; }
#status-msg { margin-bottom: 12px; padding: 10px 14px; border-radius: 6px; display: none; } #status-msg { margin-bottom: 12px; padding: 10px 14px; border-radius: 6px; }
#status-msg.ok { background: #1a4a1a; color: #4caf50; display: block; } #status-msg.ok { background: #1a4a1a; color: #4caf50; }
#status-msg.err { background: #4a1a1a; color: #f44336; display: block; } #status-msg.err { background: #4a1a1a; color: #f44336; }
input[type=email] { padding: 9px 12px; border-radius: 6px; border: 1px solid #555; background: #2a2a2a; color: #eee; font-size: 13px; width: 240px; } input[type=email] { padding: 9px 12px; border-radius: 6px; border: 1px solid #555; background: #2a2a2a; color: #eee; font-size: 13px; width: 240px; }
.ts { color: #777; font-size: 11px; } .ts { color: #777; font-size: 11px; }
</style> </style>
@@ -57,7 +57,7 @@
<input id="single-email" type="email" placeholder="user@domain.com" /> <input id="single-email" type="email" placeholder="user@domain.com" />
<button class="secondary" onclick="pushSingle()">Push Single User</button> <button class="secondary" onclick="pushSingle()">Push Single User</button>
</div> </div>
<div id="status-msg"></div> <div id="status-msg" style="display:none;"></div>
<table> <table>
<thead><tr><th>Timestamp</th><th>User</th><th>Name</th><th>Status</th><th>Message</th></tr></thead> <thead><tr><th>Timestamp</th><th>User</th><th>Name</th><th>Status</th><th>Message</th></tr></thead>
<tbody id="log-body"><tr><td colspan="5" style="color:#777;padding:20px;">Loading...</td></tr></tbody> <tbody id="log-body"><tr><td colspan="5" style="color:#777;padding:20px;">Loading...</td></tr></tbody>
@@ -90,8 +90,10 @@
} }
function showStatus(msg, ok) { function showStatus(msg, ok) {
const el = document.getElementById('status-msg'); const el = document.getElementById('status-msg');
el.textContent = msg; el.className = ok ? 'ok' : 'err'; el.textContent = msg;
setTimeout(()=>{ el.className=''; el.textContent=''; }, 7000); el.className = ok ? 'ok' : 'err';
el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 7000);
} }
async function pushAll() { async function pushAll() {
if (!confirm('Push signatures to ALL users?')) return; if (!confirm('Push signatures to ALL users?')) return;

View File

@@ -15,7 +15,7 @@
.layout { display: flex; gap: 20px; flex-wrap: wrap; } .layout { display: flex; gap: 20px; flex-wrap: wrap; }
.panel { flex: 1; min-width: 300px; } .panel { flex: 1; min-width: 300px; }
.panel h2 { font-size: 14px; color: #C9A84C; margin-bottom: 10px; } .panel h2 { font-size: 14px; color: #C9A84C; margin-bottom: 10px; }
textarea { width: 100%; height: 360px; background: #222; color: #eee; border: 1px solid #444; border-radius: 6px; padding: 12px; font-family: monospace; font-size: 12px; resize: vertical; } textarea { width: 100%; height: 380px; background: #222; color: #eee; border: 1px solid #444; border-radius: 6px; padding: 12px; font-family: monospace; font-size: 12px; resize: vertical; }
.preview-box { background: #fff; border-radius: 6px; padding: 20px; min-height: 120px; border: 1px solid #444; } .preview-box { background: #fff; border-radius: 6px; padding: 20px; min-height: 120px; border: 1px solid #444; }
.actions { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; } .actions { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
button { background: #C9A84C; color: #111; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 14px; } button { background: #C9A84C; color: #111; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 14px; }
@@ -27,6 +27,9 @@
#status-msg.err { background: #4a1a1a; color: #f44336; display: block; } #status-msg.err { background: #4a1a1a; color: #f44336; display: block; }
.test-fields { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; } .test-fields { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.test-fields input { background: #2a2a2a; border: 1px solid #444; color: #eee; padding: 6px 10px; border-radius: 4px; font-size: 12px; width: calc(50% - 4px); } .test-fields input { background: #2a2a2a; border: 1px solid #444; color: #eee; padding: 6px 10px; border-radius: 4px; font-size: 12px; width: calc(50% - 4px); }
.logo-row { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
.logo-row input { flex: 1; background: #2a2a2a; border: 1px solid #444; color: #eee; padding: 6px 10px; border-radius: 4px; font-size: 12px; }
.logo-row label { font-size: 12px; color: #aaa; white-space: nowrap; }
</style> </style>
</head> </head>
<body> <body>
@@ -50,11 +53,15 @@
<div class="panel"> <div class="panel">
<h2>Live Preview</h2> <h2>Live Preview</h2>
<div class="test-fields"> <div class="test-fields">
<input id="p-name" placeholder="Full Name" value="Jason Stedwell" oninput="updatePreview()"/> <input id="p-name" placeholder="Full Name" value="Jason Stedwell" oninput="updatePreview()"/>
<input id="p-title" placeholder="Job Title" value="Director of Technical Services" oninput="updatePreview()"/> <input id="p-title" placeholder="Job Title" value="Director of Technical Services" oninput="updatePreview()"/>
<input id="p-email" placeholder="Email" value="jstedwell@messagepointmedia.com" oninput="updatePreview()"/> <input id="p-email" placeholder="Email" value="jason@messagepoint.tv" oninput="updatePreview()"/>
<input id="p-phone" placeholder="Office Phone" value="334-707-2550" oninput="updatePreview()"/> <input id="p-phone" placeholder="Office Phone" value="205-719-5000" oninput="updatePreview()"/>
<input id="p-cell" placeholder="Cell (optional)" value="" oninput="updatePreview()"/> <input id="p-cell" placeholder="Cell (optional)" value="334-707-2550" oninput="updatePreview()"/>
</div>
<div class="logo-row">
<label>Logo URL:</label>
<input id="p-logo" value="https://alwisp.com/uploads/logo.png" oninput="updatePreview()"/>
</div> </div>
<div class="preview-box" id="preview-frame">Loading preview...</div> <div class="preview-box" id="preview-frame">Loading preview...</div>
<div class="actions"> <div class="actions">
@@ -79,19 +86,20 @@
async function updatePreview() { async function updatePreview() {
const templateHtml = document.getElementById('template-editor').value; const templateHtml = document.getElementById('template-editor').value;
const userData = { const userData = {
fullName: document.getElementById('p-name').value, fullName: document.getElementById('p-name').value,
title: document.getElementById('p-title').value, title: document.getElementById('p-title').value,
email: document.getElementById('p-email').value, email: document.getElementById('p-email').value,
phone: document.getElementById('p-phone').value, phone: document.getElementById('p-phone').value,
cellPhone: document.getElementById('p-cell').value cellPhone: document.getElementById('p-cell').value,
logoUrl: document.getElementById('p-logo').value
}; };
const res = await fetch('/api/admin/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({templateHtml,userData})}).then(r=>r.json()).catch(()=>({error:'Preview failed'})); const res = await fetch('/api/admin/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({templateHtml,userData})}).then(r=>r.json()).catch(()=>({error:'Preview failed'}));
document.getElementById('preview-frame').innerHTML = res.html || `<span style="color:red">${res.error}</span>`; document.getElementById('preview-frame').innerHTML = res.html || `<span style="color:red">${res.error}</span>`;
} }
function showStatus(msg, ok) { function showStatus(msg, ok) {
const el = document.getElementById('status-msg'); const el = document.getElementById('status-msg');
el.textContent = msg; el.className = ok ? 'ok' : 'err'; el.textContent = msg; el.className = ok ? 'ok' : 'err'; el.style.display = 'block';
setTimeout(()=>{ el.className=''; el.textContent=''; }, 5000); setTimeout(()=>{ el.style.display='none'; }, 5000);
} }
loadTemplate(); loadTemplate();
</script> </script>

View File

@@ -1,4 +1,3 @@
require('dotenv').config();
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const basicAuth = require('express-basic-auth'); const basicAuth = require('express-basic-auth');

View File

@@ -44,10 +44,10 @@ router.post('/preview', (req, res) => {
const template = Handlebars.compile(templateHtml); const template = Handlebars.compile(templateHtml);
const rendered = template(userData || { const rendered = template(userData || {
fullName: 'Jason Stedwell', fullName: 'Jason Stedwell',
title: 'Director of Technical Services', title: 'Director of Technical Services (IT/Systems/Engineering)',
email: 'jstedwell@messagepointmedia.com', email: 'jason@messagepoint.tv',
phone: '334-707-2550', phone: '205-719-5000',
cellPhone: '' cellPhone: '334-707-2550'
}); });
res.json({ html: rendered }); res.json({ html: rendered });
} catch (err) { } catch (err) {

View File

@@ -31,15 +31,15 @@ async function getAllUsers() {
} while (pageToken); } while (pageToken);
return users.map(u => ({ return users.map(u => ({
email: u.primaryEmail, email: u.primaryEmail,
fullName: u.name?.fullName || '', fullName: u.name?.fullName || '',
firstName: u.name?.givenName || '', firstName: u.name?.givenName || '',
lastName: u.name?.familyName || '', lastName: u.name?.familyName || '',
title: u.organizations?.[0]?.title || '', title: u.organizations?.[0]?.title || '',
department: u.organizations?.[0]?.department || '', department: u.organizations?.[0]?.department || '',
phone: u.phones?.find(p => p.type === 'work')?.value || '', phone: u.phones?.find(p => p.type === 'work')?.value || '',
cellPhone: u.phones?.find(p => p.type === 'mobile')?.value || '', cellPhone: u.phones?.find(p => p.type === 'mobile')?.value || '',
suspended: u.suspended || false suspended: u.suspended || false
})); }));
} }

View File

@@ -1,21 +1,27 @@
<table cellpadding="0" cellspacing="0" border="0" style="font-family: Arial, sans-serif; font-size: 13px; color: #333333;"> <table cellpadding="0" cellspacing="0" border="0"
style="font-family: Arial, sans-serif; font-size: 13px; color: #333333; max-width: 480px;">
<tr> <tr>
<td style="padding-right: 16px; vertical-align: middle; border-right: 2px solid #C9A84C;"> <td style="padding-right: 14px; vertical-align: middle; border-right: 2px solid #C9A84C;">
<img src="cid:company-logo" alt="Messagepoint Media" <img src="https://alwisp.com/uploads/logo.png"
style="width: 80px; height: auto; display: block;" alt="Messagepoint Media"
onerror="this.style.display='none'" /> width="72"
style="width: 72px; height: auto; display: block;" />
</td> </td>
<td style="padding-left: 16px; vertical-align: middle;"> <td style="padding-left: 14px; vertical-align: middle; line-height: 1.6;">
<p style="margin: 0 0 2px 0; font-weight: bold; font-size: 14px; color: #1a1a1a;">{{fullName}}</p> <p style="margin: 0; font-weight: bold; font-size: 14px; color: #1a1a1a;">{{fullName}}</p>
<p style="margin: 0 0 2px 0; color: #555555; font-size: 12px;">{{title}}</p> {{#if_val title}}
<p style="margin: 0 0 2px 0;"> <p style="margin: 0; color: #555555; font-size: 12px;">{{title}}</p>
<a href="mailto:{{email}}" style="color: #C9A84C; text-decoration: none;">{{email}}</a> {{/if_val}}
{{#if_val email}}
<p style="margin: 0;">
<a href="mailto:{{email}}" style="color: #C9A84C; text-decoration: none; font-size: 13px;">{{email}}</a>
</p> </p>
{{/if_val}}
{{#if_val phone}} {{#if_val phone}}
<p style="margin: 0 0 2px 0; color: #333333;">O: {{phone}}</p> <p style="margin: 0; color: #333333; font-size: 13px;">O: {{phone}}</p>
{{/if_val}} {{/if_val}}
{{#if_val cellPhone}} {{#if_val cellPhone}}
<p style="margin: 0 0 2px 0; color: #333333;">C: {{cellPhone}}</p> <p style="margin: 0; color: #333333; font-size: 13px;">C: {{cellPhone}}</p>
{{/if_val}} {{/if_val}}
</td> </td>
</tr> </tr>