---
name: "3M OMG Portal"
client: 3M
status: active
server: 3m-portal
tech: [Node.js, Vanilla JavaScript, HTML/CSS, SQLite, One2Edit API, Mailgun]
local_path: /Users/ai_leed/Documents/Projects/Oliver/3m-portal
deploy: "sudo bash /opt/3m-portal/deploy.sh --update"
url: https://3m.automation.oliver.solutions
tags: [3m, one2edit, translation, proxy, nodejs, sqlite, auth]
created: 2026-04-14
last_commit: 2026-05-05
port: 3000
---

## Overview

Web portal for 3M to manage One2Edit translation jobs. Provides real email/password authentication, per-user job filtering, admin user management, and an embedded One2Edit editor. Backend is Node.js with SQLite sessions, Mailgun for invite/reset emails, and an authenticated proxy to the One2Edit API.

> [!info] Production
> **URL:** https://3m.automation.oliver.solutions
> **Admin:** https://3m.automation.oliver.solutions/admin.html
> **Server:** `ssh 3m-portal` → `10.220.168.7`

## Tech Stack

- **Frontend:** Vanilla JS (ES6), HTML5, CSS3 — no build step
- **Backend:** Node.js (stdlib `http` module, no Express)
- **Database:** SQLite via `better-sqlite3` at `/opt/3m-portal/data/portal.db`
- **Auth:** Email/password → scrypt hash → HttpOnly cookie (`portal_session`) → SQLite session
- **Email:** Mailgun HTTP API (no SDK) — invites + password resets
- **Proxy:** Authenticated relay to `https://oliver.one2edit.com/v3/Api.php`
- **Process manager:** PM2 (`nodeapp` user)
- **Web server:** Apache reverse proxy on port 80

## Architecture

```
Browser
  ├── /api/auth/*   → lib/routes/auth.js
  ├── /api/admin/*  → lib/routes/admin.js   (admin role only)
  ├── /api          → lib/proxy.js → One2Edit API  (requires auth)
  └── static files  → login.html, dashboard.html, editor.html, admin.html …
```

**Auth flow:**
1. `POST /api/auth/login` → verify scrypt hash → create session row → set HttpOnly cookie
2. Every request: `lib/auth-middleware.js` reads cookie → looks up session → attaches `req.user`
3. On login: `one2edit.userInfo(username)` + `one2edit.externSessionAdd(username)` → externSessionId cached in session
4. `/api/auth/me` returns `{id, email, role, one2editUserId, externSessionId}` — dashboard/editor use this

**Roles:** `admin` (invite/manage users) + `user`

**Invite flow:** admin creates user → Mailgun magic-link email (7d TTL) → user sets password → auto-logged in

## Server

| | |
|---|---|
| **SSH alias** | `ssh 3m-portal` |
| **IP** | `10.220.168.7` |
| **User** | `vadym.samoilenko` (sudo root) |
| **App dir** | `/opt/3m-portal/` |
| **Data dir** | `/opt/3m-portal/data/portal.db` |
| **PM2 process** | `3m-portal` (runs as `nodeapp`) |
| **Logs** | `sudo pm2 logs 3m-portal` |
| **Apache conf** | `/etc/apache2/sites-available/3m.automation.oliver.solutions.conf` |

## Deployment

```bash
# Upload code from local (no Bitbucket key on server):
rsync -avz --delete \
  --exclude='.git' --exclude='node_modules' \
  --exclude='data' --exclude='.env' \
  -e "ssh -i ~/.ssh/id_rsa_vadym" \
  ./ vadym.samoilenko@10.220.168.7:/opt/3m-portal/

# Then on server:
ssh 3m-portal
sudo bash /opt/3m-portal/deploy.sh --update
```

> [!warning] Git pull не работает
> На сервере нет SSH-ключа для Bitbucket. Код загружается через `rsync` с локальной машины.

**First-time install:**
```bash
sudo bash /opt/3m-portal/deploy.sh   # installs Node, PM2, Apache vhost, creates data/
```

## Environment Variables

Файл: `/opt/3m-portal/.env` (mode 600, owner nodeapp)

| Variable | Value |
|---|---|
| `SERVICE_USERNAME` | `portal@oliver.agency` |
| `SERVICE_PASSWORD` | (в .env на сервере) |
| `PORT` | `3000` |
| `APP_BASE_URL` | `https://3m.automation.oliver.solutions` |
| `DATA_DIR` | `/opt/3m-portal/data` |
| `COOKIE_NAME` | `portal_session` |
| `COOKIE_SECURE` | `true` |
| `SESSION_TTL_MS` | `28800000` (8h) |
| `INITIAL_ADMINS` | JSON array — удалить после первого логина |
| `MAILGUN_API_KEY` | (в .env на сервере) |
| `MAILGUN_DOMAIN` | `mg.oliver.solutions` |
| `MAILGUN_FROM` | `noreply@mg.oliver.solutions` |

## Key Files

| File/Dir | Purpose |
|---|---|
| `server.js` | HTTP server: boot, routing, static serving |
| `lib/db.js` | SQLite schema, migrations, DB helpers |
| `lib/session.js` | Cookie session CRUD (8h sliding TTL) |
| `lib/tokens.js` | Invite/reset one-time tokens |
| `lib/auth-middleware.js` | `requireAuth`, `requireAdmin`, `requireFullSession` |
| `lib/proxy.js` | Authenticated One2Edit XML proxy |
| `lib/routes/auth.js` | `/api/auth/*` — login, logout, me, forgot/reset/change-password, invite |
| `lib/routes/admin.js` | `/api/admin/*` — user list, create+invite, patch, resend |
| `lib/one2edit.js` | `userInfo`, `externSessionAdd`, `externSessionRemove` |
| `lib/mailer.js` | Mailgun HTTP API (invite + reset emails) |
| `lib/password.js` | scrypt hash/verify |
| `lib/audit.js` | Audit log writes |
| `lib/rate-limit.js` | In-memory token bucket |
| `lib/router.js` | Minimal HTTP router with `:param` support |
| `scripts/create-admin.js` | Emergency CLI to create/reset admin |
| `auth.js` | Login page JS |
| `dashboard.js` | Job list, filtering, PDF export |
| `admin.js` | Admin console: user table + invite modal |

## Database

SQLite at `/opt/3m-portal/data/portal.db`

Tables: `users`, `sessions`, `tokens`, `audit_log`

```bash
# Check users on server:
ssh 3m-portal
sudo sqlite3 /opt/3m-portal/data/portal.db 'SELECT id,email,role,is_active FROM users;'

# Recent audit events:
sudo sqlite3 /opt/3m-portal/data/portal.db \
  'SELECT action,actor_user_id,datetime(created_at/1000,"unixepoch") FROM audit_log ORDER BY id DESC LIMIT 20;'
```

## Admin Credentials

| | |
|---|---|
| **Email** | `PaulJohns@oliver.agency` |
| **Password** | `CHANGE_ME` (forced change on first login) |
| **One2Edit username** | `PaulJohns@oliver.agency` |

> [!warning] После первого логина Пола
> Удалить `INITIAL_ADMINS` из `.env` на сервере и перезапустить:
> `ssh 3m-portal && sudo pm2 reload 3m-portal --update-env`

## Useful Commands

```bash
# SSH to server
ssh 3m-portal

# PM2 status & logs
sudo pm2 list
sudo pm2 logs 3m-portal --lines 50

# Reload after code update (zero-downtime)
sudo pm2 reload 3m-portal --update-env

# Emergency admin creation
ssh 3m-portal
cd /opt/3m-portal && sudo -u nodeapp node scripts/create-admin.js
```

## Pages

| Page | URL |
|---|---|
| Login | `/login.html` |
| Dashboard | `/dashboard.html` |
| Editor | `/editor.html` |
| Admin console | `/admin.html` |
| Change password | `/change-password.html` |
| Forgot password | `/forgot-password.html` |
| Reset password | `/reset-password.html?token=…` |
| Accept invite | `/accept-invite.html?token=…` |

## Related

- [[hm-o2e-tool/HM O2E Tool]] — same One2Edit platform

---

## Sessions
### 2026-05-05 – Can a test account be created
**Asked:** Can a test account be created without using a live email address?
**Done:** Confirmed test account creation is possible using the create-admin.js script which bypasses email verification by writing directly to SQLite.

### 2026-05-05 – Asked | Can a test account
**Asked:** Asked | Can a test account be created without a live email?
**Done:** Done | Reviewed admin creation script and invite process to determine if test accounts are possible without live email.

### 2026-05-05 – Review and apply updated Live_v1 version
**Asked:** Review and apply updated Live_v1 version to project, push to git, then plan login page implementation for 3M site client.
**Done:** Analyzed requirements and identified need for secure login page with username passing and dynamic data loading before proposing implementation plan.


### 2026-05-05 – Full auth system implementation + production deploy
**Asked:** Реализовать полноценную auth-систему (SQLite сессии, email-инвайты, admin-консоль, Mailgun). Задеплоить на продакшн-сервер 10.220.168.7 без потери данных.
**Done:**
- Реализован полный стек авторизации: SQLite (`better-sqlite3`), scrypt пароли, HttpOnly cookie сессии, роли admin/user
- Добавлены все backend-модули: `lib/db.js`, `lib/session.js`, `lib/tokens.js`, `lib/auth-middleware.js`, `lib/routes/auth.js`, `lib/routes/admin.js`, `lib/one2edit.js`, `lib/mailer.js`, `lib/password.js`, `lib/audit.js`, `lib/rate-limit.js`, `lib/router.js`, `lib/proxy.js`
- Новые страницы: `admin.html`, `change-password.html`, `forgot-password.html`, `reset-password.html`, `accept-invite.html`
- Удалены legacy: `api.js`, `login.js`; обновлены `auth.js`, `dashboard.js`, `editor.html`, `login.html`
- Первый admin: `PaulJohns@oliver.agency` (O2E username подтверждён через API — ID=9)
- Продакшн-деплой: rsync → git pull (после fix chown) → npm install (better-sqlite3 собрался) → pm2 reload
- Обнаружено: git pull через SSH не работает (нет ключа Bitbucket на сервере) → добавлен алиас `ssh 3m-portal` в `~/.ssh/config`
- Bootstrap создал admin-аккаунт автоматически при первом старте

### 2026-05-05 – Check and apply an updated version
**Asked:** Check and apply an updated version of Live_v1 to the project, then push to git.
**Done:** Applied 9 files with 303 new lines including timeout configs, named constants, and auto-refresh features.

### 2026-04-14 – Project catalogued
**Done:** Added to Obsidian second brain with full details.

---

## Change Log

| Date | Changed |
|---|---|
| 2026-05-05 | Test account creation | Located create-admin.js script, documented email bypass method | scripts/create-admin.js |
| 2026-05-05 | Test account creation | Documented create-admin.js script usage with sample command and parameters | scripts/create-admin.js |
| 2026-05-05 | Test account creation | Reviewed admin script and invite process | Admin creation script, invite process |
| 2026-05-05 | Login page implementation | Secure authentication, username state passing, dynamic data loading | TBD pending clarification questions |
| 2026-05-05 | Full auth system: SQLite, scrypt, sessions, admin console, Mailgun invites — production deploy |
| 2026-05-05 | Live_v1 update: search/filter, auto-refresh, timeout handling |
| 2026-03-11 | Add missing image assets (3M_Splash.jpg, login_logo.png) |
