diff --git a/.gitattributes b/.gitattributes index cce0ef3a..9ce14a9d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,5 @@ # Normalize line endings to LF on commit * text=auto eol=lf - # Explicitly enforce LF for source files *.ts text eol=lf *.tsx text eol=lf @@ -14,7 +13,6 @@ *.yaml text eol=lf *.py text eol=lf *.sh text eol=lf - # Binary files — no line ending conversion *.png binary *.jpg binary @@ -27,3 +25,4 @@ *.eot binary *.pdf binary *.zip binary +.github/assets/TREK1.gif filter=lfs diff=lfs merge=lfs -text diff --git a/.github/assets/TREK1.gif b/.github/assets/TREK1.gif new file mode 100644 index 00000000..14d50026 --- /dev/null +++ b/.github/assets/TREK1.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9153871a41ca2c53ab9188ea400eb45f4065680eae0ee0ebc3fbcf18373d99c +size 95418702 diff --git a/README.md b/README.md index 46a969a5..14649089 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,160 @@ -
-
-
- Your Trips. Your Plan.
-
+
- A self-hosted, real-time collaborative travel planner with interactive maps, budgets, packing lists, and more.
-
- Live Demo — Try TREK without installing. Resets hourly.
-
+
+| + +#### 🧭 Trip planning + +- **Drag & drop planner** — organise places into day plans with reordering and cross-day moves +- **Interactive map** — Leaflet or Mapbox GL with 3D buildings, terrain, photo markers, clustering, route visualization +- **Place search** — Google Places (photos, ratings, hours) or OpenStreetMap (free, no API key) +- **Day notes** — timestamped, icon-tagged notes with drag-and-drop reordering +- **Route optimisation** — auto-sort places and export to Google Maps +- **Weather forecasts** — 16-day via Open-Meteo (no key) + historical climate fallback +- **Category filter** — show only matching pins on the map + + | ++ +#### 🧳 Travel management + +- **Reservations** — flights, accommodations, restaurants with status, confirmation numbers, files +- **Budget tracking** — category-based expenses with pie chart, per-person / per-day splits, multi-currency +- **Packing lists** — categories, templates, user assignment, progress tracking +- **Bag tracking** — optional weight tracking with iOS-style distribution +- **Document manager** — attach docs, tickets, PDFs to trips / places / reservations (≤ 50 MB each) +- **PDF export** — full trip plan as PDF with cover page, images, notes + + | +
| + +#### 👥 Collaboration + +- **Real-time sync** — WebSocket. Changes appear instantly across all connected users +- **Multi-user trips** — invite members with role-based access +- **Invite links** — one-time or reusable links with expiry +- **SSO (OIDC)** — Google, Apple, Authentik, Keycloak, or any OIDC provider +- **2FA** — TOTP + backup codes +- **Collab suite** — group chat, shared notes, polls, day check-ins + + | ++ +#### 📱 Mobile & PWA + +- **Installable** — iOS and Android, straight from the browser, no App Store needed +- **Offline support** — Service Worker caches tiles, API, uploads via Workbox +- **Native feel** — fullscreen standalone, themed status bar, splash screen +- **Touch optimised** — mobile-specific layouts with safe-area handling + + | +
| + +#### 🧩 Addons (admin-toggleable) + +- **Vacay** — personal vacation planner with calendar, 100+ country holidays, carry-over tracking +- **Atlas** — world map of visited countries, bucket list, travel stats, streak tracking, liquid-glass UI +- **Collab** — chat, notes, polls, day-by-day attendance +- **Journey** — magazine-style travel journal with entries, photos, maps, moods +- **Dashboard widgets** — currency converter and timezone clocks + + | ++ +#### 🤖 AI / MCP + +- **Built-in MCP server** — OAuth 2.1 authenticated. 80+ tools, 27 resources +- **Granular scopes** — 24 OAuth scopes across 13 permission groups +- **Full automation** — AI can create trips, plan days, build packing lists, manage budgets, mark countries visited +- **Pre-built prompts** — `trip-summary`, `packing-list`, `budget-overview` +- **Addon-aware** — exposes Atlas, Collab, Vacay when those addons are on + + | +
| + +#### ⚙️ Admin & customisation + +- **Dashboard views** — card grid or compact list · **Dark mode** — full theme with matching status bar +- **14 languages** — EN, DE, ES, FR, IT, NL, HU, RU, ZH, ZH-TW, PL, CS, AR (RTL), BR, ID +- **Admin panel** — users, invites, packing templates, categories, addons, API keys, backups, GitHub history +- **Auto-backups** — scheduled with configurable retention · **Units** — °C/°F, 12h/24h, map tile sources, default coordinates + + | +|
+ {t('login.forgotPasswordSentBody')} +
+ {smtpConfigured === false && ( ++ {t('login.forgotPasswordSmtpHintOff')} +
++ {t('login.forgotPasswordBody')} +
+ {smtpConfigured === false && ( ++ {t('login.forgotPasswordSmtpHintOff')} +
++ {t('login.resetPasswordSuccessBody')} +
+ ++ {t('login.resetPasswordInvalidLinkBody')} +
+ ++ {mfaRequired ? t('login.resetPasswordMfaBody') : t('login.resetPasswordBody')} +
+ {error && ( +${safeGreeting},
+${safeBody}
++ ${safeCta} +
+${safeExpiry}
+${safeIgnore}
+ `; + return buildEmailHtml(subject, block, lang); +} + +/** + * Delivers a password-reset link. When SMTP is configured the user + * receives an email. When it isn't, the link is logged to stdout in a + * clearly-fenced block so the self-hosting admin can hand it off by + * other means. In both cases the caller always gets a boolean that + * indicates only whether the caller should treat delivery as + * best-effort done — the API response to the user must NOT leak it. + */ +export async function sendPasswordResetEmail( + to: string, + resetUrl: string, + userId: number | null, +): Promise<{ delivered: 'email' | 'log' | 'failed' }> { + const lang = userId ? getUserLanguage(userId) : 'en'; + const strings = PASSWORD_RESET_I18N[lang] || PASSWORD_RESET_I18N.en; + const smtpCfg = getSmtpConfig(); + + if (!smtpCfg) { + // No SMTP configured — log the link in a visually distinct block so + // the admin can relay it. Never log the associated user id/email + // content at a lower level, only what's needed. + // eslint-disable-next-line no-console + console.log( + `\n===== PASSWORD RESET LINK =====\n` + + `to: ${to}\n` + + `url: ${resetUrl}\n` + + `expires: 60 minutes\n` + + `(SMTP is not configured — deliver this link to the user manually.)\n` + + `================================\n`, + ); + logInfo(`Password reset link issued (no SMTP) for=${to}`); + return { delivered: 'log' }; + } + + try { + const skipTls = process.env.SMTP_SKIP_TLS_VERIFY === 'true' || getAppSetting('smtp_skip_tls_verify') === 'true'; + const transporter = nodemailer.createTransport({ + host: smtpCfg.host, + port: smtpCfg.port, + secure: smtpCfg.secure, + auth: smtpCfg.user ? { user: smtpCfg.user, pass: smtpCfg.pass } : undefined, + ...(skipTls ? { tls: { rejectUnauthorized: false } } : {}), + }); + await transporter.sendMail({ + from: smtpCfg.from, + to, + subject: `TREK — ${strings.subject}`, + text: `${strings.greeting}, ${to}\n\n${strings.body}\n\n${strings.ctaIntro}: ${resetUrl}\n\n${strings.expiry}\n${strings.ignore}`, + html: buildPasswordResetHtml(strings.subject, strings, to, resetUrl, lang), + }); + logInfo(`Password reset email sent to=${to}`); + return { delivered: 'email' }; + } catch (err) { + logError(`Password reset email failed to=${to}: ${err instanceof Error ? err.message : err}`); + return { delivered: 'failed' }; + } +} + export async function sendEmail(to: string, subject: string, body: string, userId?: number, navigateTarget?: string): Promise