mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be248e1ad4 | |||
| e290c7c522 | |||
| f20eb6639f | |||
| d0176d7ed6 | |||
| 8402f3bcfd | |||
| 6caa966a52 | |||
| 098918b416 | |||
| 28c7013252 | |||
| fa810c3bab | |||
| 93d5ab7fcd | |||
| 729526bd34 | |||
| c13b28ae8f | |||
| 8c7d1f8fa6 | |||
| dba655d6e8 | |||
| cb8280249f | |||
| 504195a324 | |||
| 47b880221d |
@@ -0,0 +1,21 @@
|
|||||||
|
## Description
|
||||||
|
<!-- What does this PR do? Why? -->
|
||||||
|
|
||||||
|
## Related Issue or Discussion
|
||||||
|
<!-- This project requires an issue or an approved feature request before submitting a PR. -->
|
||||||
|
<!-- For bug fixes: Closes #ISSUE_NUMBER -->
|
||||||
|
<!-- For features: Addresses discussion #DISCUSSION_NUMBER -->
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature
|
||||||
|
- [ ] Breaking change
|
||||||
|
- [ ] Documentation update
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have read the [Contributing Guidelines](https://github.com/mauriceboe/TREK/wiki/Contributing)
|
||||||
|
- [ ] My branch is [up to date with `dev`](https://github.com/mauriceboe/TREK/wiki/Development-environment#3-keep-your-fork-up-to-date)
|
||||||
|
- [ ] This PR targets the `dev` branch, not `main`
|
||||||
|
- [ ] I have tested my changes locally
|
||||||
|
- [ ] I have added/updated tests that prove my fix is effective or that my feature works
|
||||||
|
- [ ] I have updated documentation if needed
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: Close issues with unchanged bad titles
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */6 * * *' # Every 6 hours
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Close stale invalid-title issues
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const badTitles = [
|
||||||
|
"[bug]", "bug report", "bug", "issue",
|
||||||
|
"help", "question", "test", "...", "untitled"
|
||||||
|
];
|
||||||
|
|
||||||
|
const { data: issues } = await github.rest.issues.listForRepo({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
labels: 'invalid-title',
|
||||||
|
state: 'open',
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
for (const issue of issues) {
|
||||||
|
const createdAt = new Date(issue.created_at);
|
||||||
|
if (createdAt > twentyFourHoursAgo) continue; // grace period not over yet
|
||||||
|
|
||||||
|
const titleLower = issue.title.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!badTitles.includes(titleLower)) {
|
||||||
|
// Title was fixed — remove the label and move on
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
name: 'invalid-title',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still a bad title after 24h — close it
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
body: [
|
||||||
|
'## Issue closed',
|
||||||
|
'',
|
||||||
|
'This issue has been automatically closed because the title was not updated within 24 hours.',
|
||||||
|
'',
|
||||||
|
'Feel free to open a new issue with a descriptive title that summarizes the problem.',
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
state: 'closed',
|
||||||
|
state_reason: 'not_planned',
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: Close PRs with unchanged wrong base branch
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 */6 * * *' # Every 6 hours
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Close stale wrong-base-branch PRs
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data: pulls } = await github.rest.pulls.list({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
for (const pull of pulls) {
|
||||||
|
const hasLabel = pull.labels.some(l => l.name === 'wrong-base-branch');
|
||||||
|
if (!hasLabel) continue;
|
||||||
|
|
||||||
|
const createdAt = new Date(pull.created_at);
|
||||||
|
if (createdAt > twentyFourHoursAgo) continue; // grace period not over yet
|
||||||
|
|
||||||
|
// Base was fixed — remove label and move on
|
||||||
|
if (pull.base.ref !== 'main') {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pull.number,
|
||||||
|
name: 'wrong-base-branch',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still targeting main after 24h — close it
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: pull.number,
|
||||||
|
body: [
|
||||||
|
'## PR closed',
|
||||||
|
'',
|
||||||
|
'This PR has been automatically closed because the base branch was not updated to `dev` within 24 hours.',
|
||||||
|
'',
|
||||||
|
'Feel free to open a new PR targeting `dev`.',
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.pulls.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: pull.number,
|
||||||
|
state: 'closed',
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Close untitled issues
|
name: Flag issues with bad titles
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
@@ -10,58 +10,83 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
check-title:
|
check-title:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Close if title is empty or generic
|
- name: Flag or redirect issue
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const title = context.payload.issue.title.trim();
|
const title = context.payload.issue.title.trim();
|
||||||
const badTitles = [
|
|
||||||
"[bug]",
|
|
||||||
"bug report",
|
|
||||||
"bug",
|
|
||||||
"issue",
|
|
||||||
];
|
|
||||||
|
|
||||||
const featureRequestTitles = [
|
|
||||||
"feature request",
|
|
||||||
"[feature]",
|
|
||||||
"[feature request]",
|
|
||||||
"[enhancement]"
|
|
||||||
]
|
|
||||||
|
|
||||||
const titleLower = title.toLowerCase();
|
const titleLower = title.toLowerCase();
|
||||||
|
|
||||||
|
const badTitles = [
|
||||||
|
"[bug]", "bug report", "bug", "issue",
|
||||||
|
"help", "question", "test", "...", "untitled"
|
||||||
|
];
|
||||||
|
|
||||||
|
const featureRequestTitles = [
|
||||||
|
"feature request", "[feature]", "[feature request]", "[enhancement]"
|
||||||
|
];
|
||||||
|
|
||||||
if (badTitles.includes(titleLower)) {
|
if (badTitles.includes(titleLower)) {
|
||||||
|
// Ensure the label exists
|
||||||
|
try {
|
||||||
|
await github.rest.issues.getLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
name: 'invalid-title',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
await github.rest.issues.createLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
name: 'invalid-title',
|
||||||
|
color: 'e4e669',
|
||||||
|
description: 'Issue title does not meet quality standards',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.payload.issue.number,
|
||||||
|
labels: ['invalid-title'],
|
||||||
|
});
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: context.payload.issue.number,
|
issue_number: context.payload.issue.number,
|
||||||
body: "This issue was closed because no title was provided. Please re-open with a descriptive title that summarizes the problem."
|
body: [
|
||||||
|
'## Invalid title',
|
||||||
|
'',
|
||||||
|
`Your issue title \`${title}\` is too generic to be actionable.`,
|
||||||
|
'',
|
||||||
|
'Please edit the title to something descriptive that summarizes the problem — for example:',
|
||||||
|
'> _Map view crashes when zooming in on Safari 17_',
|
||||||
|
'',
|
||||||
|
'**This issue will be automatically closed in 24 hours if the title has not been updated.**',
|
||||||
|
].join('\n'),
|
||||||
});
|
});
|
||||||
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.payload.issue.number,
|
|
||||||
state: "closed",
|
|
||||||
state_reason: "not_planned"
|
|
||||||
});
|
|
||||||
} else if (featureRequestTitles.some(t => titleLower.startsWith(t))) {
|
} else if (featureRequestTitles.some(t => titleLower.startsWith(t))) {
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: context.payload.issue.number,
|
issue_number: context.payload.issue.number,
|
||||||
body: "Feature requests should be made in the [Discussions](https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests) — not as issues. This issue has been closed."
|
body: [
|
||||||
|
'## Wrong place for feature requests',
|
||||||
|
'',
|
||||||
|
'Feature requests should be submitted in [Discussions](https://github.com/mauriceboe/TREK/discussions/new?category=feature-requests), not as issues.',
|
||||||
|
'',
|
||||||
|
'This issue has been closed. Feel free to re-submit your idea in the right place!',
|
||||||
|
].join('\n'),
|
||||||
});
|
});
|
||||||
|
|
||||||
await github.rest.issues.update({
|
await github.rest.issues.update({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: context.payload.issue.number,
|
issue_number: context.payload.issue.number,
|
||||||
state: "closed",
|
state: 'closed',
|
||||||
state_reason: "not_planned"
|
state_reason: 'not_planned',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,14 +54,16 @@ jobs:
|
|||||||
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "$CURRENT → $NEW_VERSION ($BUMP)"
|
echo "$CURRENT → $NEW_VERSION ($BUMP)"
|
||||||
|
|
||||||
# Update both package.json files
|
# Update package.json files and Helm chart
|
||||||
cd server && npm version "$NEW_VERSION" --no-git-tag-version && cd ..
|
cd server && npm version "$NEW_VERSION" --no-git-tag-version && cd ..
|
||||||
cd client && npm version "$NEW_VERSION" --no-git-tag-version && cd ..
|
cd client && npm version "$NEW_VERSION" --no-git-tag-version && cd ..
|
||||||
|
sed -i "s/^version: .*/version: $NEW_VERSION/" charts/trek/Chart.yaml
|
||||||
|
sed -i "s/^appVersion: .*/appVersion: \"$NEW_VERSION\"/" charts/trek/Chart.yaml
|
||||||
|
|
||||||
# Commit and tag
|
# Commit and tag
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git add server/package.json server/package-lock.json client/package.json client/package-lock.json
|
git add server/package.json server/package-lock.json client/package.json client/package-lock.json charts/trek/Chart.yaml
|
||||||
git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
|
git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
|
||||||
git tag "v$NEW_VERSION"
|
git tag "v$NEW_VERSION"
|
||||||
git push origin main --follow-tags
|
git push origin main --follow-tags
|
||||||
@@ -151,3 +153,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Inspect manifest
|
- name: Inspect manifest
|
||||||
run: docker buildx imagetools inspect mauriceboe/trek:latest
|
run: docker buildx imagetools inspect mauriceboe/trek:latest
|
||||||
|
|
||||||
|
release-helm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: version-bump
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Publish Helm chart
|
||||||
|
uses: stefanprodan/helm-gh-pages@v1.7.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
charts_dir: charts
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
name: Enforce PR Target Branch
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, edited, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-target:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Flag or clear wrong base branch
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const base = context.payload.pull_request.base.ref;
|
||||||
|
const labels = context.payload.pull_request.labels.map(l => l.name);
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
|
||||||
|
// If the base was fixed, remove the label and let it through
|
||||||
|
if (base !== 'main') {
|
||||||
|
if (labels.includes('wrong-base-branch')) {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
name: 'wrong-base-branch',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base is main — check if this user is a maintainer
|
||||||
|
let permission = 'none';
|
||||||
|
try {
|
||||||
|
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
username: context.payload.pull_request.user.login,
|
||||||
|
});
|
||||||
|
permission = data.permission;
|
||||||
|
} catch (_) {
|
||||||
|
// User is not a collaborator — treat as 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['admin', 'write'].includes(permission)) {
|
||||||
|
console.log(`User has '${permission}' permission, skipping.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already labeled — avoid spamming on every push
|
||||||
|
if (labels.includes('wrong-base-branch')) {
|
||||||
|
core.setFailed("PR must target `dev`, not `main`.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the label exists
|
||||||
|
try {
|
||||||
|
await github.rest.issues.getLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
name: 'wrong-base-branch',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
await github.rest.issues.createLabel({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
name: 'wrong-base-branch',
|
||||||
|
color: 'd73a4a',
|
||||||
|
description: 'PR is targeting the wrong base branch',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
labels: ['wrong-base-branch'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: prNumber,
|
||||||
|
body: [
|
||||||
|
'## Wrong target branch',
|
||||||
|
'',
|
||||||
|
'This PR targets `main`, but contributions must go through `dev` first.',
|
||||||
|
'',
|
||||||
|
'To fix this, click **Edit** next to the PR title and change the base branch to `dev`.',
|
||||||
|
'',
|
||||||
|
'**This PR will be automatically closed in 24 hours if the base branch has not been updated.**',
|
||||||
|
'',
|
||||||
|
'> _If you need to merge directly to `main`, contact a maintainer._',
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
|
||||||
|
core.setFailed("PR must target `dev`, not `main`.");
|
||||||
+4
-15
@@ -9,6 +9,8 @@ Thanks for your interest in contributing! Please read these guidelines before op
|
|||||||
3. **No breaking changes** — Backwards compatibility is non-negotiable
|
3. **No breaking changes** — Backwards compatibility is non-negotiable
|
||||||
4. **Target the `dev` branch** — All PRs must be opened against `dev`, not `main`
|
4. **Target the `dev` branch** — All PRs must be opened against `dev`, not `main`
|
||||||
5. **Match the existing style** — No reformatting, no linter config changes, no "while I'm here" cleanups
|
5. **Match the existing style** — No reformatting, no linter config changes, no "while I'm here" cleanups
|
||||||
|
6. **Tests** — Your changes must include tests. The project maintains 80%+ coverage; PRs that drop it will be closed
|
||||||
|
7. **Branch up to date** — Your branch must be [up to date with `dev`](https://github.com/mauriceboe/TREK/wiki/Development-environment#3-keep-your-fork-up-to-date) before submitting a PR
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
@@ -35,22 +37,9 @@ fix(maps): correct zoom level on Safari
|
|||||||
feat(budget): add CSV export for expenses
|
feat(budget): add CSV export for expenses
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Setup
|
## Development Environment
|
||||||
|
|
||||||
```bash
|
See the [Developer Environment page](https://github.com/mauriceboe/TREK/wiki/Development-environment) for more information on setting up your development environment.
|
||||||
git clone https://github.com/mauriceboe/TREK.git
|
|
||||||
cd TREK
|
|
||||||
|
|
||||||
# Server
|
|
||||||
cd server && npm install && npm run dev
|
|
||||||
|
|
||||||
# Client (separate terminal)
|
|
||||||
cd client && npm install && npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Server: `http://localhost:3001` | Client: `http://localhost:5173`
|
|
||||||
|
|
||||||
On first run, check the server logs for the auto-generated admin credentials.
|
|
||||||
|
|
||||||
## More Details
|
## More Details
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.gg/J27gr9GH"><img src="https://img.shields.io/badge/Discord-Join%20Community-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
|
<a href="https://discord.gg/NhZBDSd4qW"><img src="https://img.shields.io/badge/Discord-Join%20Community-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
|
||||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg" alt="License: AGPL v3" /></a>
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg" alt="License: AGPL v3" /></a>
|
||||||
<a href="https://hub.docker.com/r/mauriceboe/trek"><img src="https://img.shields.io/docker/pulls/mauriceboe/trek" alt="Docker Pulls" /></a>
|
<a href="https://hub.docker.com/r/mauriceboe/trek"><img src="https://img.shields.io/docker/pulls/mauriceboe/trek" alt="Docker Pulls" /></a>
|
||||||
<a href="https://github.com/mauriceboe/TREK"><img src="https://img.shields.io/github/stars/mauriceboe/TREK" alt="GitHub Stars" /></a>
|
<a href="https://github.com/mauriceboe/TREK"><img src="https://img.shields.io/github/stars/mauriceboe/TREK" alt="GitHub Stars" /></a>
|
||||||
@@ -174,6 +174,8 @@ services:
|
|||||||
start_period: 15s
|
start_period: 15s
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This example is aimed at reverse-proxy deployments. If you access TREK directly on `http://<host>:3000` without nginx, Caddy, Traefik, or another TLS-terminating proxy in front of it, set `FORCE_HTTPS=false` and remove `TRUST_PROXY` to avoid redirects to a non-existent HTTPS endpoint.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
@@ -283,9 +285,9 @@ trek.yourdomain.com {
|
|||||||
| `TZ` | Timezone for logs, reminders and cron jobs (e.g. `Europe/Berlin`) | `UTC` |
|
| `TZ` | Timezone for logs, reminders and cron jobs (e.g. `Europe/Berlin`) | `UTC` |
|
||||||
| `LOG_LEVEL` | `info` = concise user actions, `debug` = verbose details | `info` |
|
| `LOG_LEVEL` | `info` = concise user actions, `debug` = verbose details | `info` |
|
||||||
| `ALLOWED_ORIGINS` | Comma-separated origins for CORS and email links | same-origin |
|
| `ALLOWED_ORIGINS` | Comma-separated origins for CORS and email links | same-origin |
|
||||||
| `FORCE_HTTPS` | Redirect HTTP to HTTPS behind a TLS-terminating proxy | `false` |
|
| `FORCE_HTTPS` | Redirect HTTP to HTTPS behind a TLS-terminating proxy. If you access TREK directly on `http://host:3000`, keep this `false`. | `false` |
|
||||||
| `COOKIE_SECURE` | Set to `false` to allow session cookies over plain HTTP (e.g. accessing via IP without HTTPS). Defaults to `true` in production. **Not recommended to disable in production.** | `true` |
|
| `COOKIE_SECURE` | Set to `false` to allow session cookies over plain HTTP (e.g. accessing via IP without HTTPS). Defaults to `true` in production. **Not recommended to disable in production.** | `true` |
|
||||||
| `TRUST_PROXY` | Number of trusted reverse proxies for `X-Forwarded-For` | `1` |
|
| `TRUST_PROXY` | Number of trusted reverse proxies for `X-Forwarded-For`. Use this only when TREK is actually behind a reverse proxy. | `1` |
|
||||||
| `ALLOW_INTERNAL_NETWORK` | Allow outbound requests to private/RFC-1918 IP addresses. Set to `true` if Immich or other integrated services are hosted on your local network. Loopback (`127.x`) and link-local/metadata addresses (`169.254.x`) are always blocked regardless of this setting. | `false` |
|
| `ALLOW_INTERNAL_NETWORK` | Allow outbound requests to private/RFC-1918 IP addresses. Set to `true` if Immich or other integrated services are hosted on your local network. Loopback (`127.x`) and link-local/metadata addresses (`169.254.x`) are always blocked regardless of this setting. | `false` |
|
||||||
| `APP_URL` | Public base URL of this instance (e.g. `https://trek.example.com`). Required when OIDC is enabled — must match the redirect URI registered with your IdP. Also used as the base URL for external links in email notifications. | — |
|
| `APP_URL` | Public base URL of this instance (e.g. `https://trek.example.com`). Required when OIDC is enabled — must match the redirect URI registered with your IdP. Also used as the base URL for external links in email notifications. | — |
|
||||||
| **OIDC / SSO** | | |
|
| **OIDC / SSO** | | |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: trek
|
name: trek
|
||||||
version: 0.1.0
|
version: 2.9.12
|
||||||
description: Minimal Helm chart for TREK app
|
description: Minimal Helm chart for TREK app
|
||||||
appVersion: "latest"
|
appVersion: "2.9.12"
|
||||||
@@ -27,7 +27,7 @@ spec:
|
|||||||
fsGroup: 1000
|
fsGroup: 1000
|
||||||
containers:
|
containers:
|
||||||
- name: trek
|
- name: trek
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
{{- with .Values.resources }}
|
{{- with .Values.resources }}
|
||||||
resources:
|
resources:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
image:
|
image:
|
||||||
repository: mauriceboe/trek
|
repository: mauriceboe/trek
|
||||||
tag: latest
|
# tag: latest
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
# Optional image pull secrets for private registries
|
# Optional image pull secrets for private registries
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.9.10",
|
"version": "2.9.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.9.10",
|
"version": "2.9.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-client",
|
"name": "trek-client",
|
||||||
"version": "2.9.10",
|
"version": "2.9.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ export default function App() {
|
|||||||
const { loadSettings } = useSettingsStore()
|
const { loadSettings } = useSettingsStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!location.pathname.startsWith('/shared/')) {
|
if (!location.pathname.startsWith('/shared/') && !location.pathname.startsWith('/login')) {
|
||||||
loadUser()
|
loadUser()
|
||||||
}
|
}
|
||||||
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => {
|
authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; permissions?: Record<string, PermissionLevel> }) => {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
|
|||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout()
|
logout()
|
||||||
navigate('/login')
|
navigate('/login', { state: { noRedirect: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
const toggleDarkMode = () => {
|
||||||
|
|||||||
@@ -575,7 +575,7 @@ export default function AccountTab(): React.ReactElement {
|
|||||||
try {
|
try {
|
||||||
await authApi.deleteOwnAccount()
|
await authApi.deleteOwnAccount()
|
||||||
logout()
|
logout()
|
||||||
navigate('/login')
|
navigate('/login', { state: { noRedirect: true } })
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||||
setShowDeleteConfirm(false)
|
setShowDeleteConfirm(false)
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'فشل الدخول إلى العرض التجريبي',
|
'login.demoFailed': 'فشل الدخول إلى العرض التجريبي',
|
||||||
'login.oidcSignIn': 'تسجيل الدخول عبر {name}',
|
'login.oidcSignIn': 'تسجيل الدخول عبر {name}',
|
||||||
'login.oidcOnly': 'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
|
'login.oidcOnly': 'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
|
||||||
|
'login.oidcLoggedOut': 'تم تسجيل خروجك. سجّل الدخول مجدداً عبر مزود SSO.',
|
||||||
'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل',
|
'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل',
|
||||||
'login.mfaTitle': 'المصادقة الثنائية',
|
'login.mfaTitle': 'المصادقة الثنائية',
|
||||||
'login.mfaSubtitle': 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.',
|
'login.mfaSubtitle': 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Falha no login de demonstração',
|
'login.demoFailed': 'Falha no login de demonstração',
|
||||||
'login.oidcSignIn': 'Entrar com {name}',
|
'login.oidcSignIn': 'Entrar com {name}',
|
||||||
'login.oidcOnly': 'Login por senha desativado. Use o provedor SSO.',
|
'login.oidcOnly': 'Login por senha desativado. Use o provedor SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Você foi desconectado. Entre novamente usando o provedor SSO.',
|
||||||
'login.demoHint': 'Experimente a demonstração — sem cadastro',
|
'login.demoHint': 'Experimente a demonstração — sem cadastro',
|
||||||
'login.mfaTitle': 'Autenticação em duas etapas',
|
'login.mfaTitle': 'Autenticação em duas etapas',
|
||||||
'login.mfaSubtitle': 'Digite o código de 6 dígitos do seu app autenticador.',
|
'login.mfaSubtitle': 'Digite o código de 6 dígitos do seu app autenticador.',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Přihlášení do dema se nezdařilo',
|
'login.demoFailed': 'Přihlášení do dema se nezdařilo',
|
||||||
'login.oidcSignIn': 'Přihlásit se přes {name}',
|
'login.oidcSignIn': 'Přihlásit se přes {name}',
|
||||||
'login.oidcOnly': 'Ověřování heslem je zakázáno. Přihlaste se prosím přes SSO poskytovatele.',
|
'login.oidcOnly': 'Ověřování heslem je zakázáno. Přihlaste se prosím přes SSO poskytovatele.',
|
||||||
|
'login.oidcLoggedOut': 'Byl jste odhlášen. Přihlaste se znovu přes SSO poskytovatele.',
|
||||||
'login.demoHint': 'Vyzkoušejte demo – registrace není nutná',
|
'login.demoHint': 'Vyzkoušejte demo – registrace není nutná',
|
||||||
'login.mfaTitle': 'Dvoufaktorové ověření',
|
'login.mfaTitle': 'Dvoufaktorové ověření',
|
||||||
'login.mfaSubtitle': 'Zadejte 6místný kód z vaší autentizační aplikace.',
|
'login.mfaSubtitle': 'Zadejte 6místný kód z vaší autentizační aplikace.',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Demo-Login fehlgeschlagen',
|
'login.demoFailed': 'Demo-Login fehlgeschlagen',
|
||||||
'login.oidcSignIn': 'Anmelden mit {name}',
|
'login.oidcSignIn': 'Anmelden mit {name}',
|
||||||
'login.oidcOnly': 'Passwort-Authentifizierung ist deaktiviert. Bitte melde dich über deinen SSO-Anbieter an.',
|
'login.oidcOnly': 'Passwort-Authentifizierung ist deaktiviert. Bitte melde dich über deinen SSO-Anbieter an.',
|
||||||
|
'login.oidcLoggedOut': 'Du wurdest abgemeldet. Melde dich erneut über deinen SSO-Anbieter an.',
|
||||||
'login.demoHint': 'Demo ausprobieren — ohne Registrierung',
|
'login.demoHint': 'Demo ausprobieren — ohne Registrierung',
|
||||||
'login.mfaTitle': 'Zwei-Faktor-Authentifizierung',
|
'login.mfaTitle': 'Zwei-Faktor-Authentifizierung',
|
||||||
'login.mfaSubtitle': 'Gib den 6-stelligen Code aus deiner Authenticator-App ein.',
|
'login.mfaSubtitle': 'Gib den 6-stelligen Code aus deiner Authenticator-App ein.',
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Demo login failed',
|
'login.demoFailed': 'Demo login failed',
|
||||||
'login.oidcSignIn': 'Sign in with {name}',
|
'login.oidcSignIn': 'Sign in with {name}',
|
||||||
'login.oidcOnly': 'Password authentication is disabled. Please sign in using your SSO provider.',
|
'login.oidcOnly': 'Password authentication is disabled. Please sign in using your SSO provider.',
|
||||||
|
'login.oidcLoggedOut': 'You have been logged out. Sign in again using your SSO provider.',
|
||||||
'login.demoHint': 'Try the demo — no registration needed',
|
'login.demoHint': 'Try the demo — no registration needed',
|
||||||
'login.mfaTitle': 'Two-factor authentication',
|
'login.mfaTitle': 'Two-factor authentication',
|
||||||
'login.mfaSubtitle': 'Enter the 6-digit code from your authenticator app.',
|
'login.mfaSubtitle': 'Enter the 6-digit code from your authenticator app.',
|
||||||
|
|||||||
@@ -1490,6 +1490,7 @@ const es: Record<string, string> = {
|
|||||||
'admin.oidcOnlyMode': 'Desactivar autenticación por contraseña',
|
'admin.oidcOnlyMode': 'Desactivar autenticación por contraseña',
|
||||||
'admin.oidcOnlyModeHint': 'Si está activado, solo se permite el inicio de sesión con SSO. El inicio de sesión y registro con contraseña se bloquean.',
|
'admin.oidcOnlyModeHint': 'Si está activado, solo se permite el inicio de sesión con SSO. El inicio de sesión y registro con contraseña se bloquean.',
|
||||||
'login.oidcOnly': 'La autenticación por contraseña está desactivada. Por favor, inicia sesión con tu proveedor SSO.',
|
'login.oidcOnly': 'La autenticación por contraseña está desactivada. Por favor, inicia sesión con tu proveedor SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Has cerrado sesión. Vuelve a iniciar sesión con tu proveedor SSO.',
|
||||||
|
|
||||||
// Settings (2.6.2)
|
// Settings (2.6.2)
|
||||||
'settings.currentPasswordRequired': 'La contraseña actual es obligatoria',
|
'settings.currentPasswordRequired': 'La contraseña actual es obligatoria',
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const fr: Record<string, string> = {
|
|||||||
'login.demoFailed': 'Échec de la connexion démo',
|
'login.demoFailed': 'Échec de la connexion démo',
|
||||||
'login.oidcSignIn': 'Se connecter avec {name}',
|
'login.oidcSignIn': 'Se connecter avec {name}',
|
||||||
'login.oidcOnly': 'L\'authentification par mot de passe est désactivée. Veuillez vous connecter via votre fournisseur SSO.',
|
'login.oidcOnly': 'L\'authentification par mot de passe est désactivée. Veuillez vous connecter via votre fournisseur SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Vous avez été déconnecté. Reconnectez-vous via votre fournisseur SSO.',
|
||||||
'login.demoHint': 'Essayez la démo — aucune inscription nécessaire',
|
'login.demoHint': 'Essayez la démo — aucune inscription nécessaire',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Demo bejelentkezés sikertelen',
|
'login.demoFailed': 'Demo bejelentkezés sikertelen',
|
||||||
'login.oidcSignIn': 'Bejelentkezés ezzel: {name}',
|
'login.oidcSignIn': 'Bejelentkezés ezzel: {name}',
|
||||||
'login.oidcOnly': 'A jelszavas hitelesítés le van tiltva. Kérjük, jelentkezz be az SSO szolgáltatódon keresztül.',
|
'login.oidcOnly': 'A jelszavas hitelesítés le van tiltva. Kérjük, jelentkezz be az SSO szolgáltatódon keresztül.',
|
||||||
|
'login.oidcLoggedOut': 'Kijelentkeztél. Jelentkezz be újra az SSO szolgáltatódon keresztül.',
|
||||||
'login.demoHint': 'Próbáld ki a demót — regisztráció nélkül',
|
'login.demoHint': 'Próbáld ki a demót — regisztráció nélkül',
|
||||||
'login.mfaTitle': 'Kétfaktoros hitelesítés',
|
'login.mfaTitle': 'Kétfaktoros hitelesítés',
|
||||||
'login.mfaSubtitle': 'Add meg a 6 jegyű kódot a hitelesítő alkalmazásból.',
|
'login.mfaSubtitle': 'Add meg a 6 jegyű kódot a hitelesítő alkalmazásból.',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Accesso demo fallito',
|
'login.demoFailed': 'Accesso demo fallito',
|
||||||
'login.oidcSignIn': 'Accedi con {name}',
|
'login.oidcSignIn': 'Accedi con {name}',
|
||||||
'login.oidcOnly': 'L\'autenticazione tramite password è disabilitata. Accedi utilizzando il tuo provider SSO.',
|
'login.oidcOnly': 'L\'autenticazione tramite password è disabilitata. Accedi utilizzando il tuo provider SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Sei stato disconnesso. Accedi nuovamente tramite il tuo provider SSO.',
|
||||||
'login.demoHint': 'Prova la demo — nessuna registrazione necessaria',
|
'login.demoHint': 'Prova la demo — nessuna registrazione necessaria',
|
||||||
'login.mfaTitle': 'Autenticazione a due fattori',
|
'login.mfaTitle': 'Autenticazione a due fattori',
|
||||||
'login.mfaSubtitle': 'Inserisci il codice a 6 cifre dalla tua app authenticator.',
|
'login.mfaSubtitle': 'Inserisci il codice a 6 cifre dalla tua app authenticator.',
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const nl: Record<string, string> = {
|
|||||||
'login.demoFailed': 'Demo-login mislukt',
|
'login.demoFailed': 'Demo-login mislukt',
|
||||||
'login.oidcSignIn': 'Inloggen met {name}',
|
'login.oidcSignIn': 'Inloggen met {name}',
|
||||||
'login.oidcOnly': 'Wachtwoordauthenticatie is uitgeschakeld. Log in via je SSO-provider.',
|
'login.oidcOnly': 'Wachtwoordauthenticatie is uitgeschakeld. Log in via je SSO-provider.',
|
||||||
|
'login.oidcLoggedOut': 'Je bent uitgelogd. Log opnieuw in via je SSO-provider.',
|
||||||
'login.demoHint': 'Probeer de demo — geen registratie nodig',
|
'login.demoHint': 'Probeer de demo — geen registratie nodig',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'login.demoFailed': 'Nie udało się zalogować do wersji demonstracyjnej',
|
'login.demoFailed': 'Nie udało się zalogować do wersji demonstracyjnej',
|
||||||
'login.oidcSignIn': 'Zaloguj się z {name}',
|
'login.oidcSignIn': 'Zaloguj się z {name}',
|
||||||
'login.oidcOnly': 'Uwierzytelnianie hasłem jest wyłączone. Zaloguj się za pomocą swojego dostawcy SSO.',
|
'login.oidcOnly': 'Uwierzytelnianie hasłem jest wyłączone. Zaloguj się za pomocą swojego dostawcy SSO.',
|
||||||
|
'login.oidcLoggedOut': 'Zostałeś wylogowany. Zaloguj się ponownie za pomocą swojego dostawcy SSO.',
|
||||||
'login.demoHint': 'Wypróbuj demo — nie wymaga rejestracji',
|
'login.demoHint': 'Wypróbuj demo — nie wymaga rejestracji',
|
||||||
'login.mfaTitle': 'Uwierzytelnianie dwuskładnikowe',
|
'login.mfaTitle': 'Uwierzytelnianie dwuskładnikowe',
|
||||||
'login.mfaSubtitle': 'Wprowadź 6-cyfrowy kod z aplikacji uwierzytelniającej.',
|
'login.mfaSubtitle': 'Wprowadź 6-cyfrowy kod z aplikacji uwierzytelniającej.',
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const ru: Record<string, string> = {
|
|||||||
'login.demoFailed': 'Ошибка демо-входа',
|
'login.demoFailed': 'Ошибка демо-входа',
|
||||||
'login.oidcSignIn': 'Войти через {name}',
|
'login.oidcSignIn': 'Войти через {name}',
|
||||||
'login.oidcOnly': 'Вход по паролю отключён. Используйте вашего провайдера SSO для входа.',
|
'login.oidcOnly': 'Вход по паролю отключён. Используйте вашего провайдера SSO для входа.',
|
||||||
|
'login.oidcLoggedOut': 'Вы вышли из системы. Войдите снова через вашего провайдера SSO.',
|
||||||
'login.demoHint': 'Попробуйте демо — регистрация не требуется',
|
'login.demoHint': 'Попробуйте демо — регистрация не требуется',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const zh: Record<string, string> = {
|
|||||||
'login.demoFailed': '演示登录失败',
|
'login.demoFailed': '演示登录失败',
|
||||||
'login.oidcSignIn': '通过 {name} 登录',
|
'login.oidcSignIn': '通过 {name} 登录',
|
||||||
'login.oidcOnly': '密码登录已关闭。请通过 SSO 提供商登录。',
|
'login.oidcOnly': '密码登录已关闭。请通过 SSO 提供商登录。',
|
||||||
|
'login.oidcLoggedOut': '您已退出登录。请重新通过 SSO 提供商登录。',
|
||||||
'login.demoHint': '试用演示——无需注册',
|
'login.demoHint': '试用演示——无需注册',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ const zhTw: Record<string, string> = {
|
|||||||
'login.demoFailed': '演示登入失敗',
|
'login.demoFailed': '演示登入失敗',
|
||||||
'login.oidcSignIn': '透過 {name} 登入',
|
'login.oidcSignIn': '透過 {name} 登入',
|
||||||
'login.oidcOnly': '密碼登入已關閉。請透過 SSO 提供商登入。',
|
'login.oidcOnly': '密碼登入已關閉。請透過 SSO 提供商登入。',
|
||||||
|
'login.oidcLoggedOut': '您已登出。請重新透過 SSO 提供商登入。',
|
||||||
'login.demoHint': '試用演示——無需註冊',
|
'login.demoHint': '試用演示——無需註冊',
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
|
|||||||
@@ -1551,7 +1551,7 @@ docker run -d --name trek \\
|
|||||||
await adminApi.rotateJwtSecret()
|
await adminApi.rotateJwtSecret()
|
||||||
setShowRotateJwtModal(false)
|
setShowRotateJwtModal(false)
|
||||||
logout()
|
logout()
|
||||||
navigate('/login')
|
navigate('/login', { state: { noRedirect: true } })
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('common.error'))
|
toast.error(t('common.error'))
|
||||||
setRotatingJwt(false)
|
setRotatingJwt(false)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react'
|
import React, { useState, useEffect, useMemo, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useAuthStore } from '../store/authStore'
|
import { useAuthStore } from '../store/authStore'
|
||||||
import { useSettingsStore } from '../store/settingsStore'
|
import { useSettingsStore } from '../store/settingsStore'
|
||||||
import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n'
|
import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n'
|
||||||
@@ -29,10 +29,13 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
const [appConfig, setAppConfig] = useState<AppConfig | null>(null)
|
const [appConfig, setAppConfig] = useState<AppConfig | null>(null)
|
||||||
const [inviteToken, setInviteToken] = useState<string>('')
|
const [inviteToken, setInviteToken] = useState<string>('')
|
||||||
const [inviteValid, setInviteValid] = useState<boolean>(false)
|
const [inviteValid, setInviteValid] = useState<boolean>(false)
|
||||||
|
const exchangeInitiated = useRef(false)
|
||||||
|
|
||||||
const { login, register, demoLogin, completeMfaLogin, loadUser } = useAuthStore()
|
const { login, register, demoLogin, completeMfaLogin, loadUser } = useAuthStore()
|
||||||
const { setLanguageLocal } = useSettingsStore()
|
const { setLanguageLocal } = useSettingsStore()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
const noRedirect = !!(location.state as { noRedirect?: boolean } | null)?.noRedirect
|
||||||
|
|
||||||
const redirectTarget = useMemo(() => {
|
const redirectTarget = useMemo(() => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
@@ -63,11 +66,13 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (oidcCode) {
|
if (oidcCode) {
|
||||||
|
if (exchangeInitiated.current) return
|
||||||
|
exchangeInitiated.current = true
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
window.history.replaceState({}, '', '/login')
|
|
||||||
fetch('/api/auth/oidc/exchange?code=' + encodeURIComponent(oidcCode), { credentials: 'include' })
|
fetch('/api/auth/oidc/exchange?code=' + encodeURIComponent(oidcCode), { credentials: 'include' })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(async data => {
|
.then(async data => {
|
||||||
|
window.history.replaceState({}, '', '/login')
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
await loadUser()
|
await loadUser()
|
||||||
navigate('/dashboard', { replace: true })
|
navigate('/dashboard', { replace: true })
|
||||||
@@ -75,7 +80,10 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
setError(data.error || 'OIDC login failed')
|
setError(data.error || 'OIDC login failed')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => setError('OIDC login failed'))
|
.catch(() => {
|
||||||
|
window.history.replaceState({}, '', '/login')
|
||||||
|
setError('OIDC login failed')
|
||||||
|
})
|
||||||
.finally(() => setIsLoading(false))
|
.finally(() => setIsLoading(false))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -96,12 +104,12 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
if (config) {
|
if (config) {
|
||||||
setAppConfig(config)
|
setAppConfig(config)
|
||||||
if (!config.has_users) setMode('register')
|
if (!config.has_users) setMode('register')
|
||||||
if (config.oidc_only_mode && config.oidc_configured && config.has_users && !invite) {
|
if (config.oidc_only_mode && config.oidc_configured && config.has_users && !invite && !noRedirect) {
|
||||||
window.location.href = '/api/auth/oidc/login'
|
window.location.href = '/api/auth/oidc/login'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [navigate, t])
|
}, [navigate, t, noRedirect])
|
||||||
|
|
||||||
const handleDemoLogin = async (): Promise<void> => {
|
const handleDemoLogin = async (): Promise<void> => {
|
||||||
setError('')
|
setError('')
|
||||||
@@ -527,7 +535,7 @@ export default function LoginPage(): React.ReactElement {
|
|||||||
{oidcOnly ? (
|
{oidcOnly ? (
|
||||||
<>
|
<>
|
||||||
<h2 style={{ margin: '0 0 4px', fontSize: 22, fontWeight: 800, color: '#111827' }}>{t('login.title')}</h2>
|
<h2 style={{ margin: '0 0 4px', fontSize: 22, fontWeight: 800, color: '#111827' }}>{t('login.title')}</h2>
|
||||||
<p style={{ margin: '0 0 24px', fontSize: 13.5, color: '#9ca3af' }}>{t('login.oidcOnly')}</p>
|
<p style={{ margin: '0 0 24px', fontSize: 13.5, color: '#9ca3af' }}>{noRedirect ? t('login.oidcLoggedOut') : t('login.oidcOnly')}</p>
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{ padding: '10px 14px', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 10, fontSize: 13, color: '#dc2626', marginBottom: 16 }}>
|
<div style={{ padding: '10px 14px', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 10, fontSize: 13, color: '#dc2626', marginBottom: 16 }}>
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.9.10",
|
"version": "2.9.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.9.10",
|
"version": "2.9.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.28.0",
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
||||||
"archiver": "^6.0.1",
|
"archiver": "^6.0.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trek-server",
|
"name": "trek-server",
|
||||||
"version": "2.9.10",
|
"version": "2.9.12",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --import tsx src/index.ts",
|
"start": "node --import tsx src/index.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user