| Images | ||
| lib | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| accept-invite.html | ||
| accept-invite.js | ||
| admin.html | ||
| admin.js | ||
| auth.js | ||
| change-password.html | ||
| change-password.js | ||
| CLAUDE.md | ||
| dashboard.html | ||
| dashboard.js | ||
| deploy.sh | ||
| editor.html | ||
| forgot-password.html | ||
| forgot-password.js | ||
| login.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| reset-password.html | ||
| reset-password.js | ||
| server.js | ||
| styles.css | ||
3M OMG Portal
A web portal for managing One2Edit translation jobs with 3M branding. Provides real email/password authentication, per-user job filtering, admin user management, and an embedded One2Edit editor.
Production URL: https://3m.automation.oliver.solutions
Quick start (local dev)
cp .env.example .env # fill in all values (see below)
npm install
npm start # http://localhost:3000
Environment variables
| Variable | Description |
|---|---|
SERVICE_USERNAME |
One2Edit service account email |
SERVICE_PASSWORD |
One2Edit service account password |
PORT |
HTTP port (default 3000) |
APP_BASE_URL |
Public base URL used in email links |
DATA_DIR |
Directory for SQLite DB (default ./data) |
COOKIE_NAME |
Session cookie name (default portal_session) |
COOKIE_SECURE |
Set false for local http dev, true for prod |
SESSION_TTL_MS |
Session lifetime in ms (default 28800000 = 8h) |
INITIAL_ADMINS |
JSON array of admins to seed on first boot (see below) |
MAILGUN_API_KEY |
Mailgun API key |
MAILGUN_DOMAIN |
Mailgun sending domain |
MAILGUN_FROM |
From address for emails |
INITIAL_ADMINS format
[
{"email":"admin@example.com","one2editUsername":"firstname.lastname","password":"strongpassword"},
{"email":"admin2@example.com","one2editUsername":"firstname2.lastname2","password":"strongpassword2"}
]
On first server start, if no admins exist, these accounts are created with must_change_password=1. Delete INITIAL_ADMINS from .env after the first login and password change.
Architecture
Browser → /api/auth/* → lib/routes/auth.js
Browser → /api/admin/* → lib/routes/admin.js
Browser → /api → lib/proxy.js → One2Edit API (requires auth)
Browser → static files → login.html, dashboard.html, editor.html, admin.html …
Auth: Email/password → HttpOnly cookie (portal_session) → SQLite session row
Roles: admin (full access) and user
Email: Mailgun HTTP API (no SDK) for invite and password-reset links
Pages
| Page | Description |
|---|---|
login.html |
Sign-in form |
dashboard.html |
Job list with progress, filters, PDF export |
editor.html |
One2Edit embedded editor |
admin.html |
Admin console: invite users, manage roles/status |
change-password.html |
Forced password change on first login |
forgot-password.html |
Self-service password reset request |
reset-password.html |
Reset password via emailed link |
accept-invite.html |
Accept invite and set password via emailed link |
User flows
First-time admin setup:
- Set
INITIAL_ADMINSin.envwith correct One2Edit usernames, runnpm start - Log in → forced to
/change-password.html→ set new password → dashboard
Inviting a new user (admin only):
- Go to Admin console → "Invite User"
- Enter email, One2Edit username, role → "Send Invite"
- User receives email with magic link → sets password → auto-logged in
Forgot password:
- Click "Forgot password?" on login page → enter email → receive reset link
- Click link (valid 1h) → set new password → log in
API endpoints
Public
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/login |
Login, sets session cookie |
| POST | /api/auth/forgot-password |
Request password reset email |
| POST | /api/auth/reset-password |
Consume reset token, set new password |
| POST | /api/auth/accept-invite |
Consume invite token, set password, auto-login |
| GET | /api/auth/invite-info |
Check invite token validity |
Authenticated
| Method | Path | Description |
|---|---|---|
| GET | /api/auth/me |
Current user + session info |
| POST | /api/auth/change-password |
Change password |
| POST | /api/auth/logout |
Destroy session, remove O2E session |
Admin only
| Method | Path | Description |
|---|---|---|
| GET | /api/admin/users |
List all users |
| POST | /api/admin/users |
Create user + send invite email |
| PATCH | /api/admin/users/:id |
Update role / active status |
| POST | /api/admin/users/:id/resend-invite |
Resend invite email |
| POST | /api/admin/users/:id/reset-password |
Send password reset email |
Emergency admin CLI
If you're locked out:
node scripts/create-admin.js
Prompts for email, One2Edit username, and password. Creates a new admin or resets the password of an existing user directly in the database.
Deploy
# First time (on the server, as root):
git clone git@bitbucket.org:zlalani/3m-portal.git /opt/3m-portal
# Fill in /opt/3m-portal/.env
sudo bash /opt/3m-portal/deploy.sh
# After a git pull:
sudo bash /opt/3m-portal/deploy.sh --update
The deploy script installs build-essential (required by better-sqlite3), creates /opt/3m-portal/data/ with correct permissions, runs npm install --omit=dev, configures PM2, and sets up the Apache reverse proxy.
Database
SQLite at $DATA_DIR/portal.db. Tables: users, sessions, tokens, audit_log.
Check recent audit events:
sqlite3 /opt/3m-portal/data/portal.db \
'SELECT action, actor_user_id, target_user_id, datetime(created_at/1000,"unixepoch") FROM audit_log ORDER BY id DESC LIMIT 20;'