Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3ce51d629 | |||
| 1f505b48c7 | |||
| afda7abcbe | |||
| 798d9c3ae0 | |||
| e0f22dd397 | |||
| ecda05aa46 | |||
| 9eadade2f4 | |||
| 2cc4f7c047 | |||
| 74a5fe2b80 | |||
| 50c472991a | |||
| 7dabf844a8 | |||
| 7d03541201 | |||
| d4a2c33c37 | |||
| e59290802a | |||
| 1d857d8205 | |||
| 4f9aeb7b85 | |||
| 1178eaec62 | |||
| dd1454c3cf | |||
| c539bed4d3 | |||
| 135b37edf1 | |||
| 48cf445e79 | |||
| 72708b5a99 | |||
| 948860e8ac | |||
| f7c1904625 | |||
| e3b2df4aac | |||
| 6cd4a19ed6 | |||
| 4d73c6a939 | |||
| df91ed2aac | |||
| 8639d85fe7 | |||
| e7a79d973e | |||
| 6c5394107e | |||
| 0a158e9ec3 | |||
| 48c354fbb4 | |||
| 41c42b968e | |||
| d64dc7cf45 | |||
| 6010841ee7 | |||
| 3857f1339d | |||
| 328ff92c52 | |||
| af7d6d78a8 | |||
| 01ef500793 | |||
| 1438dc7838 |
@@ -3,8 +3,8 @@
|
|||||||
<identity>
|
<identity>
|
||||||
<name>MokoGitea</name>
|
<name>MokoGitea</name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
|
<description>Moko fork of Gitea -- adding project board REST API endpoints and custom enhancements</description>
|
||||||
<version>05.50.00</version>
|
<version>05.47.00</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -445,9 +445,10 @@ server.tool(
|
|||||||
assignees: z.array(z.string()).optional().describe('Usernames to assign'),
|
assignees: z.array(z.string()).optional().describe('Usernames to assign'),
|
||||||
status_id: z.number().optional().describe('Custom status definition ID'),
|
status_id: z.number().optional().describe('Custom status definition ID'),
|
||||||
priority_id: z.number().optional().describe('Custom priority definition ID'),
|
priority_id: z.number().optional().describe('Custom priority definition ID'),
|
||||||
|
type_id: z.number().optional().describe('Custom type definition ID'),
|
||||||
...ConnectionParam,
|
...ConnectionParam,
|
||||||
},
|
},
|
||||||
async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, connection }) => {
|
async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, type_id, connection }) => {
|
||||||
const c = clientFor(connection);
|
const c = clientFor(connection);
|
||||||
|
|
||||||
// Search for existing issue with same title to prevent duplicates
|
// Search for existing issue with same title to prevent duplicates
|
||||||
@@ -483,6 +484,7 @@ server.tool(
|
|||||||
if (issueData?.id) {
|
if (issueData?.id) {
|
||||||
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-status`, { status_id });
|
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-status`, { status_id });
|
||||||
if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-priority`, { priority_id });
|
if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-priority`, { priority_id });
|
||||||
|
if (type_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-type`, { type_id });
|
||||||
}
|
}
|
||||||
const out = formatResponse(res);
|
const out = formatResponse(res);
|
||||||
out.content[0].text = `Updated existing issue #${existing.number} (duplicate prevented)\n${out.content[0].text}`;
|
out.content[0].text = `Updated existing issue #${existing.number} (duplicate prevented)\n${out.content[0].text}`;
|
||||||
@@ -501,6 +503,7 @@ server.tool(
|
|||||||
if (newIssue?.id) {
|
if (newIssue?.id) {
|
||||||
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-status`, { status_id });
|
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-status`, { status_id });
|
||||||
if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-priority`, { priority_id });
|
if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-priority`, { priority_id });
|
||||||
|
if (type_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-type`, { type_id });
|
||||||
}
|
}
|
||||||
return formatResponse(res);
|
return formatResponse(res);
|
||||||
},
|
},
|
||||||
@@ -2006,6 +2009,52 @@ server.tool(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── Issue Types (org-level) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'gitea_org_issue_types_list',
|
||||||
|
'List custom issue type definitions for an organization',
|
||||||
|
{
|
||||||
|
org: z.string().describe('Organization name'),
|
||||||
|
...ConnectionParam,
|
||||||
|
},
|
||||||
|
async ({ org, connection }) => {
|
||||||
|
const c = clientFor(connection);
|
||||||
|
return formatResponse(await c.get(`/orgs/${org}/issue-types`));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'gitea_issue_set_type',
|
||||||
|
'Set custom type on an issue',
|
||||||
|
{
|
||||||
|
owner: z.string().describe('Repository owner'),
|
||||||
|
repo: z.string().describe('Repository name'),
|
||||||
|
issue_id: z.number().describe('Internal issue ID'),
|
||||||
|
type_id: z.number().describe('Type definition ID (0 to clear)'),
|
||||||
|
...ConnectionParam,
|
||||||
|
},
|
||||||
|
async ({ owner, repo, issue_id, type_id, connection }) => {
|
||||||
|
const c = clientFor(connection);
|
||||||
|
return formatResponse(await c.post(`/repos/${owner}/${repo}/issues/${issue_id}/custom-type`, { type_id }));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Security ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'gitea_security_alerts',
|
||||||
|
'List security alerts for a repository',
|
||||||
|
{
|
||||||
|
...OwnerRepo,
|
||||||
|
...ConnectionParam,
|
||||||
|
},
|
||||||
|
async ({ owner, repo, connection }) => {
|
||||||
|
const c = clientFor(connection);
|
||||||
|
return formatResponse(await c.get(`/repos/${owner}/${repo}/security/alerts`));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ── Start Server ────────────────────────────────────────────────────────
|
// ── Start Server ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
# | joomla: XML manifest, type-prefixed packages |
|
||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
@@ -71,20 +71,25 @@ jobs:
|
|||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
echo Using pre-installed /opt/moko-platform
|
||||||
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/branch_rename.php \
|
php ${MOKO_CLI}/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
@@ -100,16 +105,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
--skip-update-stream
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
@@ -151,25 +155,60 @@ jobs:
|
|||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
# Ensure PHP + Composer are available
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
if ! command -v composer &> /dev/null; then
|
echo Using pre-installed /opt/moko-platform
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
|
|
||||||
|
|
||||||
- name: "Publish stable release"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability stable --bump minor --branch main \
|
--path . --stability stable --bump minor --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
--skip-update-stream
|
|
||||||
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||||
|
else
|
||||||
|
NOTES="Stable release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
@@ -182,7 +221,7 @@ jobs:
|
|||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php /tmp/moko-platform-api/cli/release_mirror.php \
|
php ${MOKO_CLI}/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
@@ -256,7 +295,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
php ${MOKO_CLI}/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 05.50.00
|
# VERSION: 05.47.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Publish MCP to npm
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- '.mokogitea/mcp/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install and build
|
||||||
|
working-directory: .mokogitea/mcp
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npx tsc
|
||||||
|
|
||||||
|
- name: Check version change
|
||||||
|
id: version
|
||||||
|
working-directory: .mokogitea/mcp
|
||||||
|
run: |
|
||||||
|
LOCAL_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
NPM_VERSION=$(npm view @mokoconsulting/mokogitea-mcp version 2>/dev/null || echo "0.0.0")
|
||||||
|
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
|
||||||
|
echo "changed=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
|
||||||
|
else
|
||||||
|
echo "changed=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version unchanged: $LOCAL_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
if: steps.version.outputs.changed == 'true'
|
||||||
|
working-directory: .mokogitea/mcp
|
||||||
|
run: npm publish --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish to Gitea registry
|
||||||
|
if: steps.version.outputs.changed == 'true'
|
||||||
|
working-directory: .mokogitea/mcp
|
||||||
|
run: |
|
||||||
|
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
|
||||||
|
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.GITEA_TOKEN }}
|
||||||
@@ -64,20 +64,19 @@ jobs:
|
|||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||||
if [ -f “/opt/moko-platform/cli/version_bump.php” ] && [ -f “/opt/moko-platform/vendor/autoload.php” ]; then
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
echo “Using pre-installed /opt/moko-platform”
|
echo Using pre-installed /opt/moko-platform
|
||||||
echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV”
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo “Falling back to fresh clone”
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
rm -rf /tmp/moko-platform-api
|
rm -rf /tmp/moko-platform-api
|
||||||
git clone --depth 1 --branch main --quiet \
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
“https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV”
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
@@ -118,6 +117,9 @@ jobs:
|
|||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||||
|
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
# Append suffix for output
|
# Append suffix for output
|
||||||
if [ -n "$SUFFIX" ]; then
|
if [ -n "$SUFFIX" ]; then
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
|||||||
@@ -3,6 +3,46 @@
|
|||||||
All notable changes to MokoGitea are documented here. Versions follow the format
|
All notable changes to MokoGitea are documented here. Versions follow the format
|
||||||
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
||||||
|
|
||||||
|
## [v1.26.1-moko.06.10] - 2026-06-06
|
||||||
|
|
||||||
|
* FEATURES
|
||||||
|
* feat(issues): first-class Type field with 12 auto-seeded defaults (Bug, Feature, Enhancement, Task, Documentation, Security, Roadmap, Client, Dolibarr, Infrastructure, Joomla, WaaS)
|
||||||
|
* feat(issues): first-class Status field with 13 auto-seeded defaults including 7 Pending states
|
||||||
|
* feat(issues): first-class Priority field with 4 auto-seeded defaults (Critical, High, Medium, Low)
|
||||||
|
* feat(issues): Type/Status/Priority colored badges in issue list view
|
||||||
|
* feat(issues): status dropdown replaces close/reopen button in comment form
|
||||||
|
* feat(security): built-in security scanning platform with secret scanner (15 patterns)
|
||||||
|
* feat(security): Security tab in repo navigation with alerts, scan controls
|
||||||
|
* feat(wiki): hierarchical folder navigation with sidebar tree and breadcrumbs
|
||||||
|
* feat(ui): well-known file tabs (README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG)
|
||||||
|
* feat(settings): repo manifest settings with REST API and auto-sync on push
|
||||||
|
* feat(mcp): public MCP server published to npm (@mokoconsulting/mokogitea-mcp)
|
||||||
|
* feat(mcp): SSE transport, env var config, Docker support, 120+ tools
|
||||||
|
* feat(mcp): issue dedup on create, type_id/status_id/priority_id params
|
||||||
|
|
||||||
|
* MIGRATIONS
|
||||||
|
* All org labels migrated to first-class Type/Status/Priority fields and deleted
|
||||||
|
* Type custom field (id=9) migrated to type_id and deleted
|
||||||
|
* Status custom field (id=1) deleted (replaced by first-class field)
|
||||||
|
* Priority labels migrated to priority_id
|
||||||
|
* Pending labels migrated to status definitions
|
||||||
|
* Scope labels migrated to type definitions
|
||||||
|
* Manifests populated for all 61 repos via API
|
||||||
|
|
||||||
|
* FIXES
|
||||||
|
* fix(ui): dashboard issue count badges use label spans instead of strong tags
|
||||||
|
* fix(wiki): directory check before raw redirect for folder navigation
|
||||||
|
* fix(wiki): proper display names in sidebar tree (strip dash markers)
|
||||||
|
* fix: replace non-ASCII em dashes with hyphens for hook compatibility
|
||||||
|
* fix: hookify __init__.py for stop hook JSON validation
|
||||||
|
|
||||||
|
* INFRASTRUCTURE
|
||||||
|
* npm: @mokoconsulting/mokogitea-mcp@1.1.0 and @mokoconsulting/mokowaas-mcp@1.0.0
|
||||||
|
* MCP servers consolidated under moko-platform/mcp/servers/
|
||||||
|
* Remote MCP repos renamed to hyphens
|
||||||
|
* Wiki restructured into features/, api/, operations/ folders
|
||||||
|
* Swagger API docs enabled at /api/swagger
|
||||||
|
|
||||||
## [v1.26.1-moko.06.04] - 2026-06-06
|
## [v1.26.1-moko.06.04] - 2026-06-06
|
||||||
|
|
||||||
* FEATURES
|
* FEATURES
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ type Issue struct {
|
|||||||
Status *IssueStatusDef `xorm:"-"`
|
Status *IssueStatusDef `xorm:"-"`
|
||||||
PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"`
|
PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"`
|
||||||
PriorityDef *IssuePriorityDef `xorm:"-"`
|
PriorityDef *IssuePriorityDef `xorm:"-"`
|
||||||
|
TypeID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'type_id'"`
|
||||||
|
TypeDef *IssueTypeDef `xorm:"-"`
|
||||||
IsRead bool `xorm:"-"`
|
IsRead bool `xorm:"-"`
|
||||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||||
PullRequest *PullRequest `xorm:"-"`
|
PullRequest *PullRequest `xorm:"-"`
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(IssueTypeDef))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueTypeDef defines a custom issue type at the org level.
|
||||||
|
type IssueTypeDef struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'org_id'"`
|
||||||
|
Name string `xorm:"NOT NULL"`
|
||||||
|
Color string `xorm:"VARCHAR(7)"`
|
||||||
|
Description string `xorm:"TEXT"`
|
||||||
|
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
|
||||||
|
IsDefault bool `xorm:"NOT NULL DEFAULT false 'is_default'"`
|
||||||
|
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (IssueTypeDef) TableName() string {
|
||||||
|
return "issue_type_def"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIssueTypeDefsByOrg returns active type definitions for an org.
|
||||||
|
// Auto-seeds defaults if none exist.
|
||||||
|
func GetIssueTypeDefsByOrg(ctx context.Context, orgID int64) ([]*IssueTypeDef, error) {
|
||||||
|
defs := make([]*IssueTypeDef, 0, 10)
|
||||||
|
if err := db.GetEngine(ctx).
|
||||||
|
Where("org_id = ? AND is_active = ?", orgID, true).
|
||||||
|
OrderBy("sort_order ASC, id ASC").
|
||||||
|
Find(&defs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(defs) == 0 && orgID > 0 {
|
||||||
|
if err := seedDefaultIssueTypes(ctx, orgID); err != nil {
|
||||||
|
return defs, nil
|
||||||
|
}
|
||||||
|
return GetIssueTypeDefsByOrg(ctx, orgID)
|
||||||
|
}
|
||||||
|
return defs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllIssueTypeDefsByOrg returns all type definitions (including inactive).
|
||||||
|
func GetAllIssueTypeDefsByOrg(ctx context.Context, orgID int64) ([]*IssueTypeDef, error) {
|
||||||
|
defs := make([]*IssueTypeDef, 0, 10)
|
||||||
|
return defs, db.GetEngine(ctx).
|
||||||
|
Where("org_id = ?", orgID).
|
||||||
|
OrderBy("sort_order ASC, id ASC").
|
||||||
|
Find(&defs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIssueTypeDefByID returns a single type definition.
|
||||||
|
func GetIssueTypeDefByID(ctx context.Context, id int64) (*IssueTypeDef, error) {
|
||||||
|
def := new(IssueTypeDef)
|
||||||
|
has, err := db.GetEngine(ctx).ID(id).Get(def)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return nil, db.ErrNotExist{Resource: "IssueTypeDef", ID: id}
|
||||||
|
}
|
||||||
|
return def, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateIssueTypeDef(ctx context.Context, def *IssueTypeDef) error {
|
||||||
|
_, err := db.GetEngine(ctx).Insert(def)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateIssueTypeDef(ctx context.Context, def *IssueTypeDef) error {
|
||||||
|
_, err := db.GetEngine(ctx).ID(def.ID).AllCols().Update(def)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteIssueTypeDef(ctx context.Context, id int64) error {
|
||||||
|
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET type_id = 0 WHERE type_id = ?", id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssueTypeDef))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetIssueTypeID(ctx context.Context, issueID, typeID int64) error {
|
||||||
|
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET type_id = ? WHERE id = ?", typeID, issueID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedDefaultIssueTypes(ctx context.Context, orgID int64) error {
|
||||||
|
defaults := []*IssueTypeDef{
|
||||||
|
{OrgID: orgID, Name: "Bug", Color: "#dc2626", SortOrder: 1, IsActive: true},
|
||||||
|
{OrgID: orgID, Name: "Feature", Color: "#2563eb", SortOrder: 2, IsDefault: true, IsActive: true},
|
||||||
|
{OrgID: orgID, Name: "Enhancement", Color: "#16a34a", SortOrder: 3, IsActive: true},
|
||||||
|
{OrgID: orgID, Name: "Task", Color: "#6b7280", SortOrder: 4, IsActive: true},
|
||||||
|
{OrgID: orgID, Name: "Documentation", Color: "#8b5cf6", SortOrder: 5, IsActive: true},
|
||||||
|
{OrgID: orgID, Name: "Security", Color: "#e11d48", SortOrder: 6, IsActive: true},
|
||||||
|
}
|
||||||
|
for _, d := range defaults {
|
||||||
|
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -426,6 +426,8 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(346, "Add issue status definitions table", v1_27.AddIssueStatusDefTable),
|
newMigration(346, "Add issue status definitions table", v1_27.AddIssueStatusDefTable),
|
||||||
newMigration(347, "Add repo manifest table", v1_27.AddRepoManifestTable),
|
newMigration(347, "Add repo manifest table", v1_27.AddRepoManifestTable),
|
||||||
newMigration(348, "Add issue priority definitions table", v1_27.AddIssuePriorityDefTable),
|
newMigration(348, "Add issue priority definitions table", v1_27.AddIssuePriorityDefTable),
|
||||||
|
newMigration(349, "Add security scanning tables", v1_27.AddSecurityScanningTables),
|
||||||
|
newMigration(350, "Add issue type definitions table", v1_27.AddIssueTypeDefTable),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package v1_27
|
||||||
|
|
||||||
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddSecurityScanningTables creates security_alert and security_scanner_config tables.
|
||||||
|
func AddSecurityScanningTables(x *xorm.Engine) error {
|
||||||
|
type SecurityAlert struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
|
||||||
|
Scanner string `xorm:"VARCHAR(20) NOT NULL 'scanner'"`
|
||||||
|
Severity string `xorm:"VARCHAR(10) NOT NULL 'severity'"`
|
||||||
|
Status string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'active' 'status'"`
|
||||||
|
RuleID string `xorm:"VARCHAR(100) NOT NULL 'rule_id'"`
|
||||||
|
Title string `xorm:"TEXT NOT NULL 'title'"`
|
||||||
|
Description string `xorm:"TEXT 'description'"`
|
||||||
|
FilePath string `xorm:"TEXT 'file_path'"`
|
||||||
|
LineNumber int `xorm:"'line_number'"`
|
||||||
|
CommitSHA string `xorm:"VARCHAR(64) 'commit_sha'"`
|
||||||
|
Fingerprint string `xorm:"VARCHAR(64) INDEX 'fingerprint'"`
|
||||||
|
Metadata string `xorm:"TEXT 'metadata'"`
|
||||||
|
ResolvedBy int64 `xorm:"'resolved_by'"`
|
||||||
|
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
|
||||||
|
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
|
||||||
|
}
|
||||||
|
if err := x.Sync(new(SecurityAlert)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecurityScannerConfig struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||||
|
Enabled bool `xorm:"NOT NULL DEFAULT true 'enabled'"`
|
||||||
|
BlockOnPush bool `xorm:"NOT NULL DEFAULT false 'block_on_push'"`
|
||||||
|
SecretScanner bool `xorm:"NOT NULL DEFAULT true 'secret_scanner'"`
|
||||||
|
DependScanner bool `xorm:"NOT NULL DEFAULT true 'depend_scanner'"`
|
||||||
|
CodeScanner bool `xorm:"NOT NULL DEFAULT false 'code_scanner'"`
|
||||||
|
ConfigScanner bool `xorm:"NOT NULL DEFAULT false 'config_scanner'"`
|
||||||
|
LicenseScanner bool `xorm:"NOT NULL DEFAULT false 'license_scanner'"`
|
||||||
|
CustomPatterns string `xorm:"TEXT 'custom_patterns'"`
|
||||||
|
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
|
||||||
|
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
|
||||||
|
}
|
||||||
|
return x.Sync(new(SecurityScannerConfig))
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package v1_27
|
||||||
|
|
||||||
|
import "xorm.io/xorm"
|
||||||
|
|
||||||
|
// AddIssueTypeDefTable creates the issue_type_def table and adds type_id to issues.
|
||||||
|
func AddIssueTypeDefTable(x *xorm.Engine) error {
|
||||||
|
type IssueTypeDef struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'org_id'"`
|
||||||
|
Name string `xorm:"NOT NULL"`
|
||||||
|
Color string `xorm:"VARCHAR(7)"`
|
||||||
|
Description string `xorm:"TEXT"`
|
||||||
|
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
|
||||||
|
IsDefault bool `xorm:"NOT NULL DEFAULT false 'is_default'"`
|
||||||
|
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
|
||||||
|
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
|
||||||
|
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
|
||||||
|
}
|
||||||
|
if err := x.Sync(new(IssueTypeDef)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
type Issue struct {
|
||||||
|
TypeID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'type_id'"`
|
||||||
|
}
|
||||||
|
return x.Sync(new(Issue))
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(SecurityAlert))
|
||||||
|
db.RegisterModel(new(SecurityScannerConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertSeverity represents the severity level of a security finding.
|
||||||
|
type AlertSeverity string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SeverityCritical AlertSeverity = "critical"
|
||||||
|
SeverityHigh AlertSeverity = "high"
|
||||||
|
SeverityMedium AlertSeverity = "medium"
|
||||||
|
SeverityLow AlertSeverity = "low"
|
||||||
|
SeverityInfo AlertSeverity = "info"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertStatus represents the lifecycle state of an alert.
|
||||||
|
type AlertStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlertStatusActive AlertStatus = "active"
|
||||||
|
AlertStatusResolved AlertStatus = "resolved"
|
||||||
|
AlertStatusDismissed AlertStatus = "dismissed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScannerType identifies which scanner produced a finding.
|
||||||
|
type ScannerType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScannerSecret ScannerType = "secret"
|
||||||
|
ScannerDependency ScannerType = "dependency"
|
||||||
|
ScannerCode ScannerType = "code"
|
||||||
|
ScannerConfig ScannerType = "config"
|
||||||
|
ScannerLicense ScannerType = "license"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecurityAlert stores a single security finding for a repository.
|
||||||
|
type SecurityAlert struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
|
||||||
|
Scanner ScannerType `xorm:"VARCHAR(20) NOT NULL 'scanner'"`
|
||||||
|
Severity AlertSeverity `xorm:"VARCHAR(10) NOT NULL 'severity'"`
|
||||||
|
Status AlertStatus `xorm:"VARCHAR(10) NOT NULL DEFAULT 'active' 'status'"`
|
||||||
|
RuleID string `xorm:"VARCHAR(100) NOT NULL 'rule_id'"` // e.g. "aws-access-key", "cve-2024-1234"
|
||||||
|
Title string `xorm:"TEXT NOT NULL 'title'"`
|
||||||
|
Description string `xorm:"TEXT 'description'"`
|
||||||
|
FilePath string `xorm:"TEXT 'file_path'"`
|
||||||
|
LineNumber int `xorm:"'line_number'"`
|
||||||
|
CommitSHA string `xorm:"VARCHAR(64) 'commit_sha'"`
|
||||||
|
Fingerprint string `xorm:"VARCHAR(64) INDEX 'fingerprint'"` // dedup key: hash of rule+file+content
|
||||||
|
Metadata string `xorm:"TEXT 'metadata'"` // JSON extra data
|
||||||
|
ResolvedBy int64 `xorm:"'resolved_by'"`
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SecurityAlert) TableName() string {
|
||||||
|
return "security_alert"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityScannerConfig stores per-repo scanner settings.
|
||||||
|
type SecurityScannerConfig struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
|
||||||
|
Enabled bool `xorm:"NOT NULL DEFAULT true 'enabled'"`
|
||||||
|
BlockOnPush bool `xorm:"NOT NULL DEFAULT false 'block_on_push'"` // reject push if secrets found
|
||||||
|
SecretScanner bool `xorm:"NOT NULL DEFAULT true 'secret_scanner'"`
|
||||||
|
DependScanner bool `xorm:"NOT NULL DEFAULT true 'depend_scanner'"`
|
||||||
|
CodeScanner bool `xorm:"NOT NULL DEFAULT false 'code_scanner'"`
|
||||||
|
ConfigScanner bool `xorm:"NOT NULL DEFAULT false 'config_scanner'"`
|
||||||
|
LicenseScanner bool `xorm:"NOT NULL DEFAULT false 'license_scanner'"`
|
||||||
|
CustomPatterns string `xorm:"TEXT 'custom_patterns'"` // JSON array of custom regex patterns
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
|
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SecurityScannerConfig) TableName() string {
|
||||||
|
return "security_scanner_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
// Alert queries
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// GetActiveAlerts returns all active alerts for a repo.
|
||||||
|
func GetActiveAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) {
|
||||||
|
alerts := make([]*SecurityAlert, 0, 20)
|
||||||
|
return alerts, db.GetEngine(ctx).
|
||||||
|
Where("repo_id = ? AND status = ?", repoID, AlertStatusActive).
|
||||||
|
OrderBy("severity ASC, created_unix DESC").
|
||||||
|
Find(&alerts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAlerts returns all alerts for a repo (including resolved/dismissed).
|
||||||
|
func GetAllAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) {
|
||||||
|
alerts := make([]*SecurityAlert, 0, 50)
|
||||||
|
return alerts, db.GetEngine(ctx).
|
||||||
|
Where("repo_id = ?", repoID).
|
||||||
|
OrderBy("status ASC, severity ASC, created_unix DESC").
|
||||||
|
Find(&alerts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlertByID returns a single alert.
|
||||||
|
func GetAlertByID(ctx context.Context, id int64) (*SecurityAlert, error) {
|
||||||
|
alert := new(SecurityAlert)
|
||||||
|
has, err := db.GetEngine(ctx).ID(id).Get(alert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return nil, db.ErrNotExist{Resource: "SecurityAlert", ID: id}
|
||||||
|
}
|
||||||
|
return alert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlertCountsByRepo returns count of active alerts grouped by severity.
|
||||||
|
func GetAlertCountsByRepo(ctx context.Context, repoID int64) (map[AlertSeverity]int64, error) {
|
||||||
|
type result struct {
|
||||||
|
Severity AlertSeverity `xorm:"severity"`
|
||||||
|
Count int64 `xorm:"count"`
|
||||||
|
}
|
||||||
|
var results []result
|
||||||
|
err := db.GetEngine(ctx).
|
||||||
|
Table("security_alert").
|
||||||
|
Select("severity, COUNT(*) as count").
|
||||||
|
Where("repo_id = ? AND status = ?", repoID, AlertStatusActive).
|
||||||
|
GroupBy("severity").
|
||||||
|
Find(&results)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
counts := make(map[AlertSeverity]int64)
|
||||||
|
for _, r := range results {
|
||||||
|
counts[r.Severity] = r.Count
|
||||||
|
}
|
||||||
|
return counts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdateAlert creates a new alert or updates if fingerprint exists.
|
||||||
|
func CreateOrUpdateAlert(ctx context.Context, alert *SecurityAlert) error {
|
||||||
|
if alert.Fingerprint != "" {
|
||||||
|
existing := new(SecurityAlert)
|
||||||
|
has, err := db.GetEngine(ctx).
|
||||||
|
Where("repo_id = ? AND fingerprint = ?", alert.RepoID, alert.Fingerprint).
|
||||||
|
Get(existing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
// Update existing - refresh commit SHA and keep active
|
||||||
|
existing.CommitSHA = alert.CommitSHA
|
||||||
|
existing.LineNumber = alert.LineNumber
|
||||||
|
existing.Status = AlertStatusActive
|
||||||
|
_, err = db.GetEngine(ctx).ID(existing.ID).
|
||||||
|
Cols("commit_sha", "line_number", "status").Update(existing)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := db.GetEngine(ctx).Insert(alert)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAlertStatus changes the status of an alert.
|
||||||
|
func UpdateAlertStatus(ctx context.Context, id int64, status AlertStatus, resolvedBy int64) error {
|
||||||
|
_, err := db.GetEngine(ctx).ID(id).
|
||||||
|
Cols("status", "resolved_by").
|
||||||
|
Update(&SecurityAlert{Status: status, ResolvedBy: resolvedBy})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
// Scanner config queries
|
||||||
|
// ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// GetScannerConfig returns the scanner config for a repo, or defaults.
|
||||||
|
func GetScannerConfig(ctx context.Context, repoID int64) (*SecurityScannerConfig, error) {
|
||||||
|
cfg := new(SecurityScannerConfig)
|
||||||
|
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return &SecurityScannerConfig{
|
||||||
|
RepoID: repoID,
|
||||||
|
Enabled: true,
|
||||||
|
SecretScanner: true,
|
||||||
|
DependScanner: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveScannerConfig creates or updates scanner config.
|
||||||
|
func SaveScannerConfig(ctx context.Context, cfg *SecurityScannerConfig) error {
|
||||||
|
existing := new(SecurityScannerConfig)
|
||||||
|
has, err := db.GetEngine(ctx).Where("repo_id = ?", cfg.RepoID).Get(existing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
cfg.ID = existing.ID
|
||||||
|
_, err = db.GetEngine(ctx).ID(cfg.ID).AllCols().Update(cfg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = db.GetEngine(ctx).Insert(cfg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -1584,6 +1584,7 @@
|
|||||||
"repo.issues.save": "Save",
|
"repo.issues.save": "Save",
|
||||||
"repo.issues.status": "Status",
|
"repo.issues.status": "Status",
|
||||||
"repo.issues.priority": "Priority",
|
"repo.issues.priority": "Priority",
|
||||||
|
"repo.issues.type": "Type",
|
||||||
"repo.issues.label_title": "Name",
|
"repo.issues.label_title": "Name",
|
||||||
"repo.issues.label_description": "Description",
|
"repo.issues.label_description": "Description",
|
||||||
"repo.issues.label_color": "Color",
|
"repo.issues.label_color": "Color",
|
||||||
@@ -1969,6 +1970,7 @@
|
|||||||
"repo.signing.wont_sign.approved": "The merge will not be signed as the PR is not approved.",
|
"repo.signing.wont_sign.approved": "The merge will not be signed as the PR is not approved.",
|
||||||
"repo.ext_wiki": "Access to External Wiki",
|
"repo.ext_wiki": "Access to External Wiki",
|
||||||
"repo.ext_wiki.desc": "Link to an external wiki.",
|
"repo.ext_wiki.desc": "Link to an external wiki.",
|
||||||
|
"repo.security": "Security",
|
||||||
"repo.wiki": "Wiki",
|
"repo.wiki": "Wiki",
|
||||||
"repo.wiki.welcome": "Welcome to the Wiki.",
|
"repo.wiki.welcome": "Welcome to the Wiki.",
|
||||||
"repo.wiki.welcome_desc": "The wiki lets you write and share documentation with collaborators.",
|
"repo.wiki.welcome_desc": "The wiki lets you write and share documentation with collaborators.",
|
||||||
@@ -1992,6 +1994,7 @@
|
|||||||
"repo.wiki.page_already_exists": "A wiki page with the same name already exists.",
|
"repo.wiki.page_already_exists": "A wiki page with the same name already exists.",
|
||||||
"repo.wiki.reserved_page": "The wiki page name \"%s\" is reserved.",
|
"repo.wiki.reserved_page": "The wiki page name \"%s\" is reserved.",
|
||||||
"repo.wiki.pages": "Pages",
|
"repo.wiki.pages": "Pages",
|
||||||
|
"repo.wiki.folder_empty": "This folder is empty.",
|
||||||
"repo.wiki.last_updated": "Last updated %s",
|
"repo.wiki.last_updated": "Last updated %s",
|
||||||
"repo.wiki.page_name_desc": "Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'.",
|
"repo.wiki.page_name_desc": "Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'.",
|
||||||
"repo.wiki.original_git_entry_tooltip": "View original Git file instead of using friendly link.",
|
"repo.wiki.original_git_entry_tooltip": "View original Git file instead of using friendly link.",
|
||||||
@@ -2749,6 +2752,28 @@
|
|||||||
"repo.settings.manifest_entry_point": "Entry Point",
|
"repo.settings.manifest_entry_point": "Entry Point",
|
||||||
"repo.settings.manifest_save": "Save Manifest",
|
"repo.settings.manifest_save": "Save Manifest",
|
||||||
"repo.settings.manifest_saved": "Manifest settings saved.",
|
"repo.settings.manifest_saved": "Manifest settings saved.",
|
||||||
|
"repo.settings.security": "Security",
|
||||||
|
"repo.settings.security_desc": "Security scanning detects secrets, vulnerabilities, and code issues across the repository.",
|
||||||
|
"repo.settings.security_scanners": "Scanners",
|
||||||
|
"repo.settings.security_enabled": "Enable security scanning",
|
||||||
|
"repo.settings.security_secret_scanner": "Secret Scanner - API keys, tokens, passwords, private keys",
|
||||||
|
"repo.settings.security_depend_scanner": "Dependency Scanner - CVEs in dependencies (coming soon)",
|
||||||
|
"repo.settings.security_code_scanner": "Code Scanner - SQL injection, XSS, command injection (coming soon)",
|
||||||
|
"repo.settings.security_config_scanner": "Config Scanner - Insecure settings, debug modes (coming soon)",
|
||||||
|
"repo.settings.security_license_scanner": "License Scanner - License compliance (coming soon)",
|
||||||
|
"repo.settings.security_block_on_push": "Block pushes with critical findings",
|
||||||
|
"repo.settings.security_block_on_push_help": "Reject pushes to the default branch if critical secrets are detected.",
|
||||||
|
"repo.settings.security_save": "Save Settings",
|
||||||
|
"repo.settings.security_saved": "Security settings saved.",
|
||||||
|
"repo.settings.security_alerts": "Security Alerts",
|
||||||
|
"repo.settings.security_scan_now": "Scan Now",
|
||||||
|
"repo.settings.security_scan_complete": "Security scan complete.",
|
||||||
|
"repo.settings.security_severity": "Severity",
|
||||||
|
"repo.settings.security_scanner_type": "Scanner",
|
||||||
|
"repo.settings.security_finding": "Finding",
|
||||||
|
"repo.settings.security_file": "File",
|
||||||
|
"repo.settings.security_status": "Status",
|
||||||
|
"repo.settings.security_no_alerts": "No security alerts found. Run a scan or push to the default branch to check.",
|
||||||
"repo.settings.metadata": "Metadata",
|
"repo.settings.metadata": "Metadata",
|
||||||
"repo.settings.metadata_saved": "Repository metadata saved.",
|
"repo.settings.metadata_saved": "Repository metadata saved.",
|
||||||
"repo.settings.metadata_empty": "No metadata fields defined. Org admins can add fields in Organization Settings > Custom Fields.",
|
"repo.settings.metadata_empty": "No metadata fields defined. Org admins can add fields in Organization Settings > Custom Fields.",
|
||||||
@@ -2967,6 +2992,18 @@
|
|||||||
"org.settings.issue_priority_created": "Issue priority created.",
|
"org.settings.issue_priority_created": "Issue priority created.",
|
||||||
"org.settings.issue_priority_updated": "Issue priority updated.",
|
"org.settings.issue_priority_updated": "Issue priority updated.",
|
||||||
"org.settings.issue_priority_deleted": "Issue priority deleted.",
|
"org.settings.issue_priority_deleted": "Issue priority deleted.",
|
||||||
|
"org.settings.issue_types": "Issue Types",
|
||||||
|
"org.settings.issue_types_desc": "Define issue types for all repositories in this organization.",
|
||||||
|
"org.settings.issue_types_empty": "No custom issue types defined yet.",
|
||||||
|
"org.settings.issue_type_add": "Add Type",
|
||||||
|
"org.settings.issue_type_name": "Type Name",
|
||||||
|
"org.settings.issue_type_color": "Color",
|
||||||
|
"org.settings.issue_type_description": "Description",
|
||||||
|
"org.settings.issue_type_default": "Default",
|
||||||
|
"org.settings.issue_type_sort_order": "Sort Order",
|
||||||
|
"org.settings.issue_type_created": "Issue type created.",
|
||||||
|
"org.settings.issue_type_updated": "Issue type updated.",
|
||||||
|
"org.settings.issue_type_deleted": "Issue type deleted.",
|
||||||
"org.settings.update_streams": "Update Server",
|
"org.settings.update_streams": "Update Server",
|
||||||
"org.settings.licensing": "Update Server",
|
"org.settings.licensing": "Update Server",
|
||||||
"org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.",
|
"org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.",
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tplOrgIssueTypes templates.TplName = "org/settings/issue_types"
|
||||||
|
|
||||||
|
func SettingsIssueTypes(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("org.settings.issue_types")
|
||||||
|
ctx.Data["PageIsOrgSettings"] = true
|
||||||
|
ctx.Data["PageIsSettingsIssueTypes"] = true
|
||||||
|
|
||||||
|
defs, err := issues_model.GetAllIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetAllIssueTypeDefsByOrg", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["IssueTypes"] = defs
|
||||||
|
ctx.HTML(http.StatusOK, tplOrgIssueTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SettingsIssueTypesCreatePost(ctx *context.Context) {
|
||||||
|
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
|
||||||
|
def := &issues_model.IssueTypeDef{
|
||||||
|
OrgID: ctx.Org.Organization.ID,
|
||||||
|
Name: ctx.FormString("name"),
|
||||||
|
Color: ctx.FormString("color"),
|
||||||
|
Description: ctx.FormString("description"),
|
||||||
|
SortOrder: sortOrder,
|
||||||
|
IsDefault: ctx.FormString("is_default") == "on",
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
if def.Name == "" {
|
||||||
|
ctx.Flash.Error("Type name is required")
|
||||||
|
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := issues_model.CreateIssueTypeDef(ctx, def); err != nil {
|
||||||
|
ctx.ServerError("CreateIssueTypeDef", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_created"))
|
||||||
|
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SettingsIssueTypesEditPost(ctx *context.Context) {
|
||||||
|
id := ctx.PathParamInt64("id")
|
||||||
|
def, err := issues_model.GetIssueTypeDefByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetIssueTypeDefByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if def.OrgID != ctx.Org.Organization.ID {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
def.Name = ctx.FormString("name")
|
||||||
|
def.Color = ctx.FormString("color")
|
||||||
|
def.Description = ctx.FormString("description")
|
||||||
|
def.IsDefault = ctx.FormString("is_default") == "on"
|
||||||
|
def.IsActive = ctx.FormString("is_active") == "on"
|
||||||
|
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
|
||||||
|
def.SortOrder = sortOrder
|
||||||
|
if err := issues_model.UpdateIssueTypeDef(ctx, def); err != nil {
|
||||||
|
ctx.ServerError("UpdateIssueTypeDef", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_updated"))
|
||||||
|
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SettingsIssueTypesDeletePost(ctx *context.Context) {
|
||||||
|
id := ctx.PathParamInt64("id")
|
||||||
|
def, err := issues_model.GetIssueTypeDefByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetIssueTypeDefByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if def.OrgID != ctx.Org.Organization.ID {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := issues_model.DeleteIssueTypeDef(ctx, id); err != nil {
|
||||||
|
ctx.ServerError("DeleteIssueTypeDef", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_deleted"))
|
||||||
|
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateIssueCustomType handles POST to set a custom type on an issue.
|
||||||
|
func UpdateIssueCustomType(ctx *context.Context) {
|
||||||
|
issueID := ctx.PathParamInt64("id")
|
||||||
|
typeID := ctx.FormInt64("type_id")
|
||||||
|
|
||||||
|
issue, err := issues_model.GetIssueByID(ctx, issueID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetIssueByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeID > 0 {
|
||||||
|
typeDef, err := issues_model.GetIssueTypeDefByID(ctx, typeID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetIssueTypeDefByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if typeDef.OrgID != ctx.Repo.Repository.OwnerID {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issues_model.SetIssueTypeID(ctx, issueID, typeID); err != nil {
|
||||||
|
ctx.ServerError("SetIssueTypeID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
|
||||||
|
}
|
||||||
@@ -536,6 +536,14 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectI
|
|||||||
}
|
}
|
||||||
ctx.Data["CustomFieldDefs"] = customFieldDefs
|
ctx.Data["CustomFieldDefs"] = customFieldDefs
|
||||||
ctx.Data["CustomFieldFilters"] = customFieldFilters
|
ctx.Data["CustomFieldFilters"] = customFieldFilters
|
||||||
|
|
||||||
|
// Load first-class field definitions for issue list badges
|
||||||
|
issueStatusDefs, _ := issues_model.GetIssueStatusDefsByOrg(ctx, repo.OwnerID)
|
||||||
|
ctx.Data["IssueStatusDefs"] = issueStatusDefs
|
||||||
|
issuePriorityDefs, _ := issues_model.GetIssuePriorityDefsByOrg(ctx, repo.OwnerID)
|
||||||
|
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
|
||||||
|
issueTypeDefs, _ := issues_model.GetIssueTypeDefsByOrg(ctx, repo.OwnerID)
|
||||||
|
ctx.Data["IssueTypeDefs"] = issueTypeDefs
|
||||||
// Build a query string fragment for cf_ params so they survive pagination/sort changes.
|
// Build a query string fragment for cf_ params so they survive pagination/sort changes.
|
||||||
cfQuery := make(url.Values)
|
cfQuery := make(url.Values)
|
||||||
for fieldID, value := range customFieldFilters {
|
for fieldID, value := range customFieldFilters {
|
||||||
|
|||||||
@@ -379,6 +379,13 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
|
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
|
||||||
|
|
||||||
|
// Load custom issue type definitions for the sidebar.
|
||||||
|
issueTypeDefs, itErr := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
|
||||||
|
if itErr != nil {
|
||||||
|
log.Error("ViewIssue: GetIssueTypeDefsByOrg: %v", itErr)
|
||||||
|
}
|
||||||
|
ctx.Data["IssueTypeDefs"] = issueTypeDefs
|
||||||
|
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
|
||||||
if err := issue.LoadAttributes(ctx); err != nil {
|
if err := issue.LoadAttributes(ctx); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
|
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tplRepoSecurity templates.TplName = "repo/security"
|
||||||
|
|
||||||
|
// Security renders the repo-level security tab showing alerts and scan controls.
|
||||||
|
func Security(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("repo.security")
|
||||||
|
ctx.Data["PageIsSecurity"] = true
|
||||||
|
|
||||||
|
repoID := ctx.Repo.Repository.ID
|
||||||
|
|
||||||
|
cfg, err := security_model.GetScannerConfig(ctx, repoID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetScannerConfig", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["ScannerConfig"] = cfg
|
||||||
|
|
||||||
|
alerts, err := security_model.GetAllAlerts(ctx, repoID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetAllAlerts", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["SecurityAlerts"] = alerts
|
||||||
|
|
||||||
|
counts, err := security_model.GetAlertCountsByRepo(ctx, repoID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetAlertCountsByRepo", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["AlertCounts"] = counts
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplRepoSecurity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityScanNow triggers an immediate scan from the security tab.
|
||||||
|
func SecurityScanNow(ctx *context.Context) {
|
||||||
|
commit := ctx.Repo.Commit
|
||||||
|
if commit == nil {
|
||||||
|
ctx.Flash.Error("No commits found")
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/security")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
security_service.ScanOnPush(ctx, ctx.Repo.Repository, commit)
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.settings.security_scan_complete"))
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/security")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityAlertUpdate changes alert status from the security tab.
|
||||||
|
func SecurityAlertUpdateTab(ctx *context.Context) {
|
||||||
|
id := ctx.PathParamInt64("id")
|
||||||
|
status := security_model.AlertStatus(ctx.FormString("status"))
|
||||||
|
|
||||||
|
if status != security_model.AlertStatusResolved && status != security_model.AlertStatusDismissed {
|
||||||
|
status = security_model.AlertStatusDismissed
|
||||||
|
}
|
||||||
|
|
||||||
|
alert, err := security_model.GetAlertByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetAlertByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if alert.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := security_model.UpdateAlertStatus(ctx, id, status, ctx.Doer.ID); err != nil {
|
||||||
|
ctx.ServerError("UpdateAlertStatus", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success("Alert updated")
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/security")
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ func ManifestSettingsPost(ctx *context.Context) {
|
|||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
Name: ctx.FormString("name"),
|
Name: ctx.FormString("name"),
|
||||||
Org: ctx.FormString("org"),
|
Org: ctx.FormString("org"),
|
||||||
Description: ctx.FormString("description"),
|
Description: ctx.Repo.Repository.Description,
|
||||||
Version: ctx.FormString("version"),
|
Version: ctx.FormString("version"),
|
||||||
LicenseSPDX: ctx.FormString("license_spdx"),
|
LicenseSPDX: ctx.FormString("license_spdx"),
|
||||||
LicenseName: ctx.FormString("license_name"),
|
LicenseName: ctx.FormString("license_name"),
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||||
|
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tplSettingsSecurity templates.TplName = "repo/settings/security"
|
||||||
|
|
||||||
|
// SecuritySettings displays the repo security scanning settings and alerts.
|
||||||
|
func SecuritySettings(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("repo.settings.security")
|
||||||
|
ctx.Data["PageIsSettingsSecurity"] = true
|
||||||
|
|
||||||
|
repoID := ctx.Repo.Repository.ID
|
||||||
|
|
||||||
|
cfg, err := security_model.GetScannerConfig(ctx, repoID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetScannerConfig", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["ScannerConfig"] = cfg
|
||||||
|
|
||||||
|
alerts, err := security_model.GetAllAlerts(ctx, repoID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetAllAlerts", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["SecurityAlerts"] = alerts
|
||||||
|
|
||||||
|
counts, err := security_model.GetAlertCountsByRepo(ctx, repoID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetAlertCountsByRepo", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["AlertCounts"] = counts
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplSettingsSecurity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySettingsPost saves security scanner configuration.
|
||||||
|
func SecuritySettingsPost(ctx *context.Context) {
|
||||||
|
cfg := &security_model.SecurityScannerConfig{
|
||||||
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
|
Enabled: ctx.FormString("enabled") == "on",
|
||||||
|
BlockOnPush: ctx.FormString("block_on_push") == "on",
|
||||||
|
SecretScanner: ctx.FormString("secret_scanner") == "on",
|
||||||
|
DependScanner: ctx.FormString("depend_scanner") == "on",
|
||||||
|
CodeScanner: ctx.FormString("code_scanner") == "on",
|
||||||
|
ConfigScanner: ctx.FormString("config_scanner") == "on",
|
||||||
|
LicenseScanner: ctx.FormString("license_scanner") == "on",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := security_model.SaveScannerConfig(ctx, cfg); err != nil {
|
||||||
|
ctx.ServerError("SaveScannerConfig", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.settings.security_saved"))
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityScanNow triggers an immediate scan of the repository.
|
||||||
|
func SecurityScanNow(ctx *context.Context) {
|
||||||
|
commit := ctx.Repo.Commit
|
||||||
|
if commit == nil {
|
||||||
|
ctx.Flash.Error("No commits found in repository")
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
security_service.ScanOnPush(ctx, ctx.Repo.Repository, commit)
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.settings.security_scan_complete"))
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityAlertUpdate changes the status of a security alert.
|
||||||
|
func SecurityAlertUpdate(ctx *context.Context) {
|
||||||
|
id := ctx.PathParamInt64("id")
|
||||||
|
status := security_model.AlertStatus(ctx.FormString("status"))
|
||||||
|
|
||||||
|
if status != security_model.AlertStatusResolved && status != security_model.AlertStatusDismissed {
|
||||||
|
status = security_model.AlertStatusDismissed
|
||||||
|
}
|
||||||
|
|
||||||
|
alert, err := security_model.GetAlertByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetAlertByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if alert.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := security_model.UpdateAlertStatus(ctx, id, status, ctx.Doer.ID); err != nil {
|
||||||
|
ctx.ServerError("UpdateAlertStatus", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success("Alert updated")
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
|
||||||
|
}
|
||||||
@@ -77,6 +77,20 @@ type PageMeta struct {
|
|||||||
UpdatedUnix timeutil.TimeStamp
|
UpdatedUnix timeutil.TimeStamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WikiTreeNode represents a node in the wiki folder tree for sidebar navigation.
|
||||||
|
type WikiTreeNode struct {
|
||||||
|
Name string
|
||||||
|
SubURL string
|
||||||
|
IsDir bool
|
||||||
|
Children []*WikiTreeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// WikiBreadcrumb represents a breadcrumb segment.
|
||||||
|
type WikiBreadcrumb struct {
|
||||||
|
Name string
|
||||||
|
SubURL string
|
||||||
|
}
|
||||||
|
|
||||||
// findEntryForFile finds the tree entry for a target filepath.
|
// findEntryForFile finds the tree entry for a target filepath.
|
||||||
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
|
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
|
||||||
entry, err := commit.GetTreeEntryByPath(target)
|
entry, err := commit.GetTreeEntryByPath(target)
|
||||||
@@ -232,6 +246,43 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
|||||||
isSideBar := pageName == "_Sidebar"
|
isSideBar := pageName == "_Sidebar"
|
||||||
isFooter := pageName == "_Footer"
|
isFooter := pageName == "_Footer"
|
||||||
|
|
||||||
|
// Build breadcrumbs for the current path
|
||||||
|
breadcrumbs := buildWikiBreadcrumbs(pageName)
|
||||||
|
ctx.Data["WikiBreadcrumbs"] = breadcrumbs
|
||||||
|
|
||||||
|
// Build folder tree for sidebar navigation
|
||||||
|
wikiTree := buildWikiTree(commit)
|
||||||
|
ctx.Data["WikiTree"] = wikiTree
|
||||||
|
|
||||||
|
// Check if path is a directory first (before file lookup)
|
||||||
|
dirEntry, _ := commit.GetTreeEntryByPath(string(pageName))
|
||||||
|
if dirEntry != nil && dirEntry.IsDir() {
|
||||||
|
// Path is a directory - try index files or show folder listing
|
||||||
|
var entry *git.TreeEntry
|
||||||
|
foundIndex := false
|
||||||
|
for _, indexName := range []string{"README", "Home", "index"} {
|
||||||
|
indexPath := wiki_service.WebPath(string(pageName) + "/" + indexName)
|
||||||
|
idxEntry, _, idxNoEntry, _ := wikiEntryByName(ctx, commit, indexPath)
|
||||||
|
if !idxNoEntry && idxEntry != nil {
|
||||||
|
pageName = indexPath
|
||||||
|
entry = idxEntry
|
||||||
|
_, displayName = wiki_service.WebPathToUserTitle(pageName)
|
||||||
|
ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName)
|
||||||
|
ctx.Data["Title"] = displayName
|
||||||
|
foundIndex = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundIndex {
|
||||||
|
ctx.Data["IsWikiFolder"] = true
|
||||||
|
ctx.Data["WikiFolderPath"] = string(pageName)
|
||||||
|
folderEntries := listWikiFolderEntries(commit, string(pageName))
|
||||||
|
ctx.Data["WikiFolderEntries"] = folderEntries
|
||||||
|
return wikiGitRepo, nil
|
||||||
|
}
|
||||||
|
_ = entry // will be used below via pageName lookup
|
||||||
|
}
|
||||||
|
|
||||||
// lookup filename in wiki - get gitTree entry , real filename
|
// lookup filename in wiki - get gitTree entry , real filename
|
||||||
entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName)
|
entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName)
|
||||||
if noEntry {
|
if noEntry {
|
||||||
@@ -479,6 +530,14 @@ func Wiki(ctx *context.Context) {
|
|||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Folder listing - no entry but IsWikiFolder flag is set
|
||||||
|
if ctx.Data["IsWikiFolder"] != nil {
|
||||||
|
if wikiGitRepo != nil {
|
||||||
|
defer wikiGitRepo.Close()
|
||||||
|
}
|
||||||
|
ctx.HTML(http.StatusOK, tplWikiView)
|
||||||
|
return
|
||||||
|
}
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.wiki")
|
ctx.Data["Title"] = ctx.Tr("repo.wiki")
|
||||||
ctx.HTML(http.StatusOK, tplWikiStart)
|
ctx.HTML(http.StatusOK, tplWikiStart)
|
||||||
@@ -752,3 +811,134 @@ func DeleteWikiPagePost(ctx *context.Context) {
|
|||||||
|
|
||||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/wiki/")
|
ctx.JSONRedirect(ctx.Repo.RepoLink + "/wiki/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildWikiBreadcrumbs creates breadcrumb segments from a wiki path.
|
||||||
|
func buildWikiBreadcrumbs(pageName wiki_service.WebPath) []WikiBreadcrumb {
|
||||||
|
parts := strings.Split(string(pageName), "/")
|
||||||
|
crumbs := make([]WikiBreadcrumb, 0, len(parts))
|
||||||
|
for i, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subURL := strings.Join(parts[:i+1], "/")
|
||||||
|
crumbs = append(crumbs, WikiBreadcrumb{
|
||||||
|
Name: part,
|
||||||
|
SubURL: subURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return crumbs
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWikiTree builds a hierarchical folder tree from the wiki git repo.
|
||||||
|
func buildWikiTree(commit *git.Commit) []*WikiTreeNode {
|
||||||
|
if commit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, err := commit.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
root := make(map[string]*WikiTreeNode)
|
||||||
|
var topLevel []*WikiTreeNode
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if entry.IsDir() {
|
||||||
|
node := &WikiTreeNode{
|
||||||
|
Name: name,
|
||||||
|
SubURL: name,
|
||||||
|
IsDir: true,
|
||||||
|
}
|
||||||
|
// List children of this directory
|
||||||
|
subTree := entry.Tree()
|
||||||
|
if subTree != nil {
|
||||||
|
children, _ := subTree.ListEntries()
|
||||||
|
for _, child := range children {
|
||||||
|
childName := child.Name()
|
||||||
|
if child.IsDir() {
|
||||||
|
node.Children = append(node.Children, &WikiTreeNode{
|
||||||
|
Name: childName,
|
||||||
|
SubURL: name + "/" + childName,
|
||||||
|
IsDir: true,
|
||||||
|
})
|
||||||
|
} else if strings.HasSuffix(childName, ".md") {
|
||||||
|
wpChild, err := wiki_service.GitPathToWebPath(childName)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, childDisplay := wiki_service.WebPathToUserTitle(wpChild)
|
||||||
|
if childDisplay == "_Sidebar" || childDisplay == "_Footer" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node.Children = append(node.Children, &WikiTreeNode{
|
||||||
|
Name: childDisplay,
|
||||||
|
SubURL: name + "/" + string(wpChild),
|
||||||
|
IsDir: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root[name] = node
|
||||||
|
topLevel = append(topLevel, node)
|
||||||
|
} else if strings.HasSuffix(name, ".md") {
|
||||||
|
wpName, err := wiki_service.GitPathToWebPath(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, displayName := wiki_service.WebPathToUserTitle(wpName)
|
||||||
|
if displayName == "_Sidebar" || displayName == "_Footer" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node := &WikiTreeNode{
|
||||||
|
Name: displayName,
|
||||||
|
SubURL: string(wpName),
|
||||||
|
IsDir: false,
|
||||||
|
}
|
||||||
|
topLevel = append(topLevel, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return topLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// listWikiFolderEntries lists the pages and subfolders in a wiki directory.
|
||||||
|
func listWikiFolderEntries(commit *git.Commit, treePath string) []PageMeta {
|
||||||
|
if commit == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tree, err := commit.SubTree(treePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, err := tree.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pages []PageMeta
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if entry.IsDir() {
|
||||||
|
pages = append(pages, PageMeta{
|
||||||
|
Name: name + "/",
|
||||||
|
SubURL: treePath + "/" + name,
|
||||||
|
GitEntryName: name,
|
||||||
|
})
|
||||||
|
} else if strings.HasSuffix(name, ".md") {
|
||||||
|
wpName, err := wiki_service.GitPathToWebPath(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, displayName := wiki_service.WebPathToUserTitle(wpName)
|
||||||
|
if displayName == "_Sidebar" || displayName == "_Footer" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pages = append(pages, PageMeta{
|
||||||
|
Name: displayName,
|
||||||
|
SubURL: treePath + "/" + string(wpName),
|
||||||
|
GitEntryName: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|||||||
@@ -1079,6 +1079,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Post("/{id}/edit", org.SettingsIssuePrioritiesEditPost)
|
m.Post("/{id}/edit", org.SettingsIssuePrioritiesEditPost)
|
||||||
m.Post("/{id}/delete", org.SettingsIssuePrioritiesDeletePost)
|
m.Post("/{id}/delete", org.SettingsIssuePrioritiesDeletePost)
|
||||||
})
|
})
|
||||||
|
m.Group("/issue-types", func() {
|
||||||
|
m.Get("", org.SettingsIssueTypes)
|
||||||
|
m.Post("", org.SettingsIssueTypesCreatePost)
|
||||||
|
m.Post("/{id}/edit", org.SettingsIssueTypesEditPost)
|
||||||
|
m.Post("/{id}/delete", org.SettingsIssueTypesDeletePost)
|
||||||
|
})
|
||||||
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
||||||
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
|
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
|
||||||
}, reqSignIn)
|
}, reqSignIn)
|
||||||
@@ -1207,6 +1213,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
|
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
|
||||||
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
|
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
|
||||||
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
|
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
|
||||||
|
m.Group("/security", func() {
|
||||||
|
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
|
||||||
|
m.Post("/scan", repo_setting.SecurityScanNow)
|
||||||
|
m.Post("/alert/{id}", repo_setting.SecurityAlertUpdate)
|
||||||
|
})
|
||||||
|
|
||||||
m.Group("/collaboration", func() {
|
m.Group("/collaboration", func() {
|
||||||
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
|
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
|
||||||
@@ -1414,6 +1425,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField)
|
m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField)
|
||||||
m.Post("/{id}/custom-status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomStatus)
|
m.Post("/{id}/custom-status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomStatus)
|
||||||
m.Post("/{id}/custom-priority", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomPriority)
|
m.Post("/{id}/custom-priority", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomPriority)
|
||||||
|
m.Post("/{id}/custom-type", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomType)
|
||||||
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
|
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
|
||||||
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
|
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
|
||||||
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
|
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
|
||||||
@@ -1672,6 +1684,13 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
})
|
})
|
||||||
// end "/{username}/{reponame}/wiki"
|
// end "/{username}/{reponame}/wiki"
|
||||||
|
|
||||||
|
m.Group("/{username}/{reponame}/security", func() {
|
||||||
|
m.Get("", repo.Security)
|
||||||
|
m.Post("/scan", reqRepoAdmin, repo.SecurityScanNow)
|
||||||
|
m.Post("/alert/{id}", reqRepoAdmin, repo.SecurityAlertUpdateTab)
|
||||||
|
}, reqSignIn, context.RepoAssignment, reqRepoAdmin)
|
||||||
|
// end "/{username}/{reponame}/security"
|
||||||
|
|
||||||
m.Group("/{username}/{reponame}/activity", func() {
|
m.Group("/{username}/{reponame}/activity", func() {
|
||||||
// activity has its own permission checks
|
// activity has its own permission checks
|
||||||
m.Get("", repo.Activity)
|
m.Get("", repo.Activity)
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
|
||||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/optional"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||||
@@ -166,6 +169,18 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
|
|||||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||||
ctx.Data["Title"] = "Page Not Found"
|
ctx.Data["Title"] = "Page Not Found"
|
||||||
ctx.Data["ErrorMsg"] = "" // FIXME: the template never renders this message, need to fix in the future (and show safe messages to end users)
|
ctx.Data["ErrorMsg"] = "" // FIXME: the template never renders this message, need to fix in the future (and show safe messages to end users)
|
||||||
|
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
|
||||||
|
|
||||||
|
// Load OAuth2 providers for the login form on error pages
|
||||||
|
if !ctx.IsSigned {
|
||||||
|
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("NotFound: GetOAuth2Providers: %v", err)
|
||||||
|
}
|
||||||
|
ctx.Data["OAuth2Providers"] = oauth2Providers
|
||||||
|
ctx.Data["EnableSSPI"] = auth_model.IsSSPIEnabled(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusNotFound, "status/404")
|
ctx.HTML(http.StatusNotFound, "status/404")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +202,17 @@ func (ctx *Context) Forbidden() {
|
|||||||
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||||
ctx.Data["Title"] = "Access Denied"
|
ctx.Data["Title"] = "Access Denied"
|
||||||
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
|
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
|
||||||
|
|
||||||
|
// Load OAuth2 providers for the login form on the 403 page
|
||||||
|
if !ctx.IsSigned {
|
||||||
|
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Forbidden: GetOAuth2Providers: %v", err)
|
||||||
|
}
|
||||||
|
ctx.Data["OAuth2Providers"] = oauth2Providers
|
||||||
|
ctx.Data["EnableSSPI"] = auth_model.IsSSPIEnabled(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusForbidden, "status/403")
|
ctx.HTML(http.StatusForbidden, "status/403")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
|
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
|
||||||
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
|
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
|
||||||
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
|
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
|
||||||
|
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pushQueue represents a queue to handle update pull request tests
|
// pushQueue represents a queue to handle update pull request tests
|
||||||
@@ -195,6 +196,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
|||||||
}
|
}
|
||||||
// Auto-sync .mokogitea/manifest.xml to database on default branch push
|
// Auto-sync .mokogitea/manifest.xml to database on default branch push
|
||||||
SyncManifestFromCommit(ctx, repo, newCommit)
|
SyncManifestFromCommit(ctx, repo, newCommit)
|
||||||
|
// Run security scanners on default branch push
|
||||||
|
security_service.ScanOnPush(ctx, repo, newCommit)
|
||||||
} else {
|
} else {
|
||||||
if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
|
if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
|
||||||
log.Error("DelDivergenceFromCache: %v", err)
|
log.Error("DelDivergenceFromCache: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||||
|
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScanOnPush runs enabled scanners against a commit pushed to the default branch.
|
||||||
|
// Called from services/repository/push.go on default branch pushes.
|
||||||
|
func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
|
||||||
|
if commit == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := security_model.GetScannerConfig(ctx, repo.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("SecurityScan: GetScannerConfig for %s: %v", repo.FullName(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cfg.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanners []Scanner
|
||||||
|
if cfg.SecretScanner {
|
||||||
|
scanners = append(scanners, NewSecretScanner())
|
||||||
|
}
|
||||||
|
// Future scanners added here:
|
||||||
|
// if cfg.DependScanner { scanners = append(scanners, NewDependencyScanner()) }
|
||||||
|
// if cfg.CodeScanner { scanners = append(scanners, NewCodeScanner()) }
|
||||||
|
|
||||||
|
if len(scanners) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
totalFindings := 0
|
||||||
|
for _, s := range scanners {
|
||||||
|
findings, err := s.ScanTree(commit)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("SecurityScan: %s scanner for %s: %v", s.Type(), repo.FullName(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range findings {
|
||||||
|
alert := &security_model.SecurityAlert{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Scanner: f.Scanner,
|
||||||
|
Severity: f.Severity,
|
||||||
|
RuleID: f.RuleID,
|
||||||
|
Title: f.Title,
|
||||||
|
Description: f.Description,
|
||||||
|
FilePath: f.FilePath,
|
||||||
|
LineNumber: f.LineNumber,
|
||||||
|
CommitSHA: f.CommitSHA,
|
||||||
|
Fingerprint: f.Fingerprint,
|
||||||
|
Metadata: f.Metadata,
|
||||||
|
}
|
||||||
|
if err := security_model.CreateOrUpdateAlert(ctx, alert); err != nil {
|
||||||
|
log.Error("SecurityScan: CreateOrUpdateAlert: %v", err)
|
||||||
|
}
|
||||||
|
totalFindings++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalFindings > 0 {
|
||||||
|
log.Warn("SecurityScan: %d findings in %s", totalFindings, repo.FullName())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Finding represents a single security issue found by a scanner.
|
||||||
|
type Finding struct {
|
||||||
|
Scanner security_model.ScannerType
|
||||||
|
Severity security_model.AlertSeverity
|
||||||
|
RuleID string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
FilePath string
|
||||||
|
LineNumber int
|
||||||
|
CommitSHA string
|
||||||
|
Fingerprint string // unique identifier for dedup
|
||||||
|
Metadata string // JSON extra data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanner is the interface all security scanner modules implement.
|
||||||
|
type Scanner interface {
|
||||||
|
// Type returns the scanner type identifier.
|
||||||
|
Type() security_model.ScannerType
|
||||||
|
|
||||||
|
// ScanCommit scans a single commit and returns findings.
|
||||||
|
ScanCommit(commit *git.Commit) ([]Finding, error)
|
||||||
|
|
||||||
|
// ScanTree scans the full repository tree and returns findings.
|
||||||
|
ScanTree(commit *git.Commit) ([]Finding, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||||
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecretRule defines a pattern to match against file contents.
|
||||||
|
type SecretRule struct {
|
||||||
|
ID string
|
||||||
|
Title string
|
||||||
|
Pattern *regexp.Regexp
|
||||||
|
Severity security_model.AlertSeverity
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultSecretRules contains the built-in secret detection patterns.
|
||||||
|
var DefaultSecretRules = []SecretRule{
|
||||||
|
// AWS
|
||||||
|
{ID: "aws-access-key", Title: "AWS Access Key ID", Severity: security_model.SeverityCritical,
|
||||||
|
Pattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`), Description: "AWS access key ID detected"},
|
||||||
|
{ID: "aws-secret-key", Title: "AWS Secret Access Key", Severity: security_model.SeverityCritical,
|
||||||
|
Pattern: regexp.MustCompile(`(?i)aws_secret_access_key\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}`), Description: "AWS secret access key detected"},
|
||||||
|
|
||||||
|
// Generic tokens/keys
|
||||||
|
{ID: "private-key", Title: "Private Key", Severity: security_model.SeverityCritical,
|
||||||
|
Pattern: regexp.MustCompile(`-----BEGIN (RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----`), Description: "Private key file detected"},
|
||||||
|
{ID: "generic-api-key", Title: "Generic API Key", Severity: security_model.SeverityHigh,
|
||||||
|
Pattern: regexp.MustCompile(`(?i)(api[_-]?key|apikey)\s*[=:]\s*['"]?[A-Za-z0-9_\-]{20,}`), Description: "API key assignment detected"},
|
||||||
|
{ID: "generic-secret", Title: "Generic Secret", Severity: security_model.SeverityHigh,
|
||||||
|
Pattern: regexp.MustCompile(`(?i)(secret|password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]`), Description: "Hardcoded secret or password detected"},
|
||||||
|
{ID: "generic-token", Title: "Generic Token", Severity: security_model.SeverityHigh,
|
||||||
|
Pattern: regexp.MustCompile(`(?i)(token|auth_token|access_token)\s*[=:]\s*['"]?[A-Za-z0-9_\-.]{20,}`), Description: "Token assignment detected"},
|
||||||
|
|
||||||
|
// GitHub/Gitea
|
||||||
|
{ID: "github-pat", Title: "GitHub Personal Access Token", Severity: security_model.SeverityCritical,
|
||||||
|
Pattern: regexp.MustCompile(`ghp_[A-Za-z0-9]{36}`), Description: "GitHub personal access token detected"},
|
||||||
|
{ID: "github-oauth", Title: "GitHub OAuth Token", Severity: security_model.SeverityCritical,
|
||||||
|
Pattern: regexp.MustCompile(`gho_[A-Za-z0-9]{36}`), Description: "GitHub OAuth token detected"},
|
||||||
|
|
||||||
|
// Stripe
|
||||||
|
{ID: "stripe-secret", Title: "Stripe Secret Key", Severity: security_model.SeverityCritical,
|
||||||
|
Pattern: regexp.MustCompile(`sk_live_[A-Za-z0-9]{24,}`), Description: "Stripe live secret key detected"},
|
||||||
|
{ID: "stripe-publishable", Title: "Stripe Publishable Key", Severity: security_model.SeverityLow,
|
||||||
|
Pattern: regexp.MustCompile(`pk_live_[A-Za-z0-9]{24,}`), Description: "Stripe live publishable key detected (usually safe but flagged)"},
|
||||||
|
|
||||||
|
// JWT
|
||||||
|
{ID: "jwt-token", Title: "JWT Token", Severity: security_model.SeverityMedium,
|
||||||
|
Pattern: regexp.MustCompile(`eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}`), Description: "JWT token detected"},
|
||||||
|
|
||||||
|
// Connection strings
|
||||||
|
{ID: "connection-string", Title: "Connection String with Password", Severity: security_model.SeverityCritical,
|
||||||
|
Pattern: regexp.MustCompile(`(?i)(mysql|postgres|postgresql|mongodb|redis|amqp|smtp)://[^:]+:[^@]+@[^\s]+`), Description: "Database/service connection string with embedded password"},
|
||||||
|
|
||||||
|
// Google
|
||||||
|
{ID: "google-api-key", Title: "Google API Key", Severity: security_model.SeverityHigh,
|
||||||
|
Pattern: regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`), Description: "Google API key detected"},
|
||||||
|
|
||||||
|
// Slack
|
||||||
|
{ID: "slack-webhook", Title: "Slack Webhook URL", Severity: security_model.SeverityMedium,
|
||||||
|
Pattern: regexp.MustCompile(`https://hooks\.slack\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+`), Description: "Slack webhook URL detected"},
|
||||||
|
|
||||||
|
// SendGrid
|
||||||
|
{ID: "sendgrid-api-key", Title: "SendGrid API Key", Severity: security_model.SeverityHigh,
|
||||||
|
Pattern: regexp.MustCompile(`SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}`), Description: "SendGrid API key detected"},
|
||||||
|
|
||||||
|
// PayPal
|
||||||
|
{ID: "paypal-client-secret", Title: "PayPal Client Secret", Severity: security_model.SeverityCritical,
|
||||||
|
Pattern: regexp.MustCompile(`(?i)paypal.*secret\s*[=:]\s*['"]?[A-Za-z0-9_-]{20,}`), Description: "PayPal client secret detected"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files to skip during scanning.
|
||||||
|
var skipExtensions = map[string]bool{
|
||||||
|
".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".ico": true,
|
||||||
|
".svg": true, ".woff": true, ".woff2": true, ".ttf": true, ".eot": true,
|
||||||
|
".zip": true, ".tar": true, ".gz": true, ".bz2": true, ".7z": true,
|
||||||
|
".pdf": true, ".doc": true, ".docx": true, ".xls": true, ".xlsx": true,
|
||||||
|
".exe": true, ".dll": true, ".so": true, ".dylib": true, ".o": true,
|
||||||
|
".min.js": true, ".min.css": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var skipPaths = []string{
|
||||||
|
"vendor/", "node_modules/", ".git/", "dist/", "build/",
|
||||||
|
"go.sum", "package-lock.json", "composer.lock", "yarn.lock",
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretScanner implements the Scanner interface for secret detection.
|
||||||
|
type SecretScanner struct {
|
||||||
|
Rules []SecretRule
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSecretScanner creates a scanner with default rules.
|
||||||
|
func NewSecretScanner() *SecretScanner {
|
||||||
|
return &SecretScanner{Rules: DefaultSecretRules}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecretScanner) Type() security_model.ScannerType {
|
||||||
|
return security_model.ScannerSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecretScanner) ScanCommit(commit *git.Commit) ([]Finding, error) {
|
||||||
|
// For push-time scanning, we scan the diff of the commit
|
||||||
|
return s.ScanTree(commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecretScanner) ScanTree(commit *git.Commit) ([]Finding, error) {
|
||||||
|
if commit == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := commit.ListEntriesRecursiveFast()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ListEntriesRecursiveFast: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var findings []Finding
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsRegular() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path := entry.Name()
|
||||||
|
if shouldSkipFile(path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip large files (> 1MB)
|
||||||
|
if entry.Blob().Size() > 1024*1024 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := entry.Blob().DataAsync()
|
||||||
|
if err != nil {
|
||||||
|
log.Trace("SecretScanner: skip %s: %v", path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fileFindings := s.scanReader(reader, path, commit.ID.String())
|
||||||
|
reader.Close()
|
||||||
|
findings = append(findings, fileFindings...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return findings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecretScanner) scanReader(r io.Reader, filePath, commitSHA string) []Finding {
|
||||||
|
var findings []Finding
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
lineNum := 0
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
lineNum++
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
for _, rule := range s.Rules {
|
||||||
|
if rule.Pattern.MatchString(line) {
|
||||||
|
fingerprint := fmt.Sprintf("%x", sha256.Sum256([]byte(rule.ID+":"+filePath+":"+line)))
|
||||||
|
findings = append(findings, Finding{
|
||||||
|
Scanner: security_model.ScannerSecret,
|
||||||
|
Severity: rule.Severity,
|
||||||
|
RuleID: rule.ID,
|
||||||
|
Title: rule.Title,
|
||||||
|
Description: rule.Description,
|
||||||
|
FilePath: filePath,
|
||||||
|
LineNumber: lineNum,
|
||||||
|
CommitSHA: commitSHA,
|
||||||
|
Fingerprint: fingerprint[:32],
|
||||||
|
})
|
||||||
|
break // one finding per line per file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findings
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldSkipFile(path string) bool {
|
||||||
|
lower := strings.ToLower(path)
|
||||||
|
|
||||||
|
for _, skip := range skipPaths {
|
||||||
|
if strings.HasPrefix(lower, skip) || strings.Contains(lower, "/"+skip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ext := range skipExtensions {
|
||||||
|
if strings.HasSuffix(lower, ext) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
+27
-11
@@ -144,21 +144,23 @@ func WebPathToURLPath(s WebPath) string {
|
|||||||
|
|
||||||
func WebPathFromRequest(s string) WebPath {
|
func WebPathFromRequest(s string) WebPath {
|
||||||
s = util.PathJoinRelX(s)
|
s = util.PathJoinRelX(s)
|
||||||
// The old wiki code's behavior is always using %2F, instead of subdirectory.
|
// MokoGitea: support real subdirectories for hierarchical wiki navigation.
|
||||||
s = strings.ReplaceAll(s, "/", "%2F")
|
// Slashes are preserved as path separators, not escaped to %2F.
|
||||||
return WebPath(s)
|
return WebPath(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
var multiHyphenRe = regexp.MustCompile(`-{2,}`)
|
var multiHyphenRe = regexp.MustCompile(`-{2,}`)
|
||||||
var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`)
|
var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`)
|
||||||
|
var nonSlugReWithSlash = regexp.MustCompile(`[^a-zA-Z0-9+.\-/]`)
|
||||||
|
|
||||||
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
|
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
|
||||||
// Spaces and special characters become hyphens, consecutive hyphens collapse to one.
|
// Spaces and special characters become hyphens, consecutive hyphens collapse to one.
|
||||||
// Preserves: letters, digits, hyphens, plus signs (+), and dots (.)
|
// Preserves: letters, digits, hyphens, plus signs (+), dots (.), and slashes (/).
|
||||||
func sanitizeWikiTitle(title string) string {
|
func sanitizeWikiTitle(title string) string {
|
||||||
title = strings.TrimSpace(title)
|
title = strings.TrimSpace(title)
|
||||||
title = strings.ReplaceAll(title, " ", "-")
|
title = strings.ReplaceAll(title, " ", "-")
|
||||||
title = nonSlugRe.ReplaceAllString(title, "-")
|
// Preserve slashes as directory separators
|
||||||
|
title = nonSlugReWithSlash.ReplaceAllString(title, "-")
|
||||||
title = multiHyphenRe.ReplaceAllString(title, "-")
|
title = multiHyphenRe.ReplaceAllString(title, "-")
|
||||||
title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs
|
title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs
|
||||||
title = strings.Trim(title, "-+.")
|
title = strings.Trim(title, "-+.")
|
||||||
@@ -166,14 +168,28 @@ func sanitizeWikiTitle(title string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UserTitleToWebPath(base, title string) WebPath {
|
func UserTitleToWebPath(base, title string) WebPath {
|
||||||
// TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory.
|
// MokoGitea: support subdirectories - slashes in title create folder structure.
|
||||||
// So we do not add the support for writing slashes in title at the moment.
|
// Split on /, sanitize each segment, rejoin.
|
||||||
title = sanitizeWikiTitle(title)
|
parts := strings.Split(title, "/")
|
||||||
title = util.PathJoinRelX(base, escapeSegToWeb(title, false))
|
sanitized := make([]string, 0, len(parts))
|
||||||
if title == "" || title == "." {
|
for _, p := range parts {
|
||||||
title = "unnamed"
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p = sanitizeWikiTitle(p)
|
||||||
|
if p != "" {
|
||||||
|
sanitized = append(sanitized, escapeSegToWeb(p, false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return WebPath(title)
|
result := strings.Join(sanitized, "/")
|
||||||
|
if base != "" {
|
||||||
|
result = util.PathJoinRelX(base, result)
|
||||||
|
}
|
||||||
|
if result == "" || result == "." {
|
||||||
|
result = "unnamed"
|
||||||
|
}
|
||||||
|
return WebPath(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
|
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings issue-types")}}
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "org.settings.issue_types"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<p class="text grey">{{ctx.Locale.Tr "org.settings.issue_types_desc"}}</p>
|
||||||
|
|
||||||
|
{{if .IssueTypes}}
|
||||||
|
<table class="ui compact table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ctx.Locale.Tr "org.settings.issue_type_color"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "org.settings.issue_type_name"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "org.settings.issue_type_default"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "org.settings.issue_type_sort_order"}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .IssueTypes}}
|
||||||
|
<tr {{if not .IsActive}}class="tw-opacity-50"{{end}}>
|
||||||
|
<td>
|
||||||
|
{{if .Color}}<span class="tw-inline-block tw-w-4 tw-h-4 tw-rounded" style="background-color: {{.Color}}"></span>{{else}}<span class="text grey">-</span>{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{.Name}}</strong>
|
||||||
|
{{if not .IsActive}}<span class="ui mini grey label">Inactive</span>{{end}}
|
||||||
|
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .IsDefault}}<span class="ui mini blue label">{{ctx.Locale.Tr "org.settings.issue_type_default"}}</span>{{else}}<span class="text grey">-</span>{{end}}
|
||||||
|
</td>
|
||||||
|
<td>{{.SortOrder}}</td>
|
||||||
|
<td class="tw-text-right">
|
||||||
|
<form method="post" action="{{$.OrgLink}}/settings/issue-types/{{.ID}}/delete" class="tw-inline">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-placeholder"><p>{{ctx.Locale.Tr "org.settings.issue_types_empty"}}</p></div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
<h5>{{ctx.Locale.Tr "org.settings.issue_type_add"}}</h5>
|
||||||
|
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-types">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="three fields">
|
||||||
|
<div class="required field">
|
||||||
|
<label>{{ctx.Locale.Tr "org.settings.issue_type_name"}}</label>
|
||||||
|
<input name="name" required placeholder="e.g. Bug, Feature, Task">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "org.settings.issue_type_color"}}</label>
|
||||||
|
<input name="color" type="color" value="#2563eb">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "org.settings.issue_type_sort_order"}}</label>
|
||||||
|
<input name="sort_order" type="number" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="two fields">
|
||||||
|
<div class="field">
|
||||||
|
<label>{{ctx.Locale.Tr "org.settings.issue_type_description"}}</label>
|
||||||
|
<input name="description" placeholder="Help text">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox tw-mt-4">
|
||||||
|
<input name="is_default" type="checkbox">
|
||||||
|
<label>{{ctx.Locale.Tr "org.settings.issue_type_default"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.issue_type_add"}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{template "org/settings/layout_footer" .}}
|
||||||
@@ -37,6 +37,9 @@
|
|||||||
<a class="{{if .PageIsSettingsIssuePriorities}}active {{end}}item" href="{{.OrgLink}}/settings/issue-priorities">
|
<a class="{{if .PageIsSettingsIssuePriorities}}active {{end}}item" href="{{.OrgLink}}/settings/issue-priorities">
|
||||||
{{svg "octicon-flame"}} {{ctx.Locale.Tr "org.settings.issue_priorities"}}
|
{{svg "octicon-flame"}} {{ctx.Locale.Tr "org.settings.issue_priorities"}}
|
||||||
</a>
|
</a>
|
||||||
|
<a class="{{if .PageIsSettingsIssueTypes}}active {{end}}item" href="{{.OrgLink}}/settings/issue-types">
|
||||||
|
{{svg "octicon-tag"}} {{ctx.Locale.Tr "org.settings.issue_types"}}
|
||||||
|
</a>
|
||||||
{{if .EnableActions}}
|
{{if .EnableActions}}
|
||||||
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
|
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
|
||||||
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
|
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
|
||||||
|
|||||||
@@ -122,6 +122,15 @@
|
|||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if and .Permission.IsAdmin .IsSigned}}
|
||||||
|
<a class="{{if .PageIsSecurity}}active {{end}}item" href="{{.RepoLink}}/security">
|
||||||
|
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.security"}}
|
||||||
|
{{if .SecurityAlertCount}}
|
||||||
|
<span class="ui small label red">{{CountFmt .SecurityAlertCount}}</span>
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}}
|
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}}
|
||||||
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item">
|
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item">
|
||||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{{if .IssueTypeDefs}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
|
||||||
|
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.type"}}</span>
|
||||||
|
{{$canModify := and .FieldEditFlags .FieldEditFlags.CustomFields}}
|
||||||
|
{{if $canModify}}
|
||||||
|
<form method="post" action="{{.RepoLink}}/issues/{{.Issue.ID}}/custom-type" class="tw-inline">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<select name="type_id" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
|
||||||
|
<option value="0">-</option>
|
||||||
|
{{range .IssueTypeDefs}}
|
||||||
|
<option value="{{.ID}}" {{if eq .ID $.Issue.TypeID}}selected{{end}}
|
||||||
|
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
|
||||||
|
{{.Name}}
|
||||||
|
</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
{{$found := false}}
|
||||||
|
{{range .IssueTypeDefs}}
|
||||||
|
{{if eq .ID $.Issue.TypeID}}
|
||||||
|
{{if .Color}}<span class="tw-inline-block tw-w-3 tw-h-3 tw-rounded" style="background-color: {{.Color}}"></span>{{end}}
|
||||||
|
<span class="tw-text-sm">{{.Name}}</span>
|
||||||
|
{{$found = true}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if not $found}}
|
||||||
|
<span class="tw-text-sm text grey">-</span>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
@@ -86,23 +86,32 @@
|
|||||||
<div class="flex-text-block tw-justify-end">
|
<div class="flex-text-block tw-justify-end">
|
||||||
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .DisableStatusChange)}}
|
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .DisableStatusChange)}}
|
||||||
{{if and .IssueStatusDefs (not .Issue.IsPull)}}
|
{{if and .IssueStatusDefs (not .Issue.IsPull)}}
|
||||||
<select name="status_id" class="ui compact dropdown" style="min-width:140px;padding:7px 10px;border:1px solid var(--color-secondary);border-radius:4px;background:var(--color-body);">
|
{{if and .Issue.IsClosed (not .HasIssuesOrPullsWritePermission)}}
|
||||||
<option value="">-- {{ctx.Locale.Tr "repo.issues.status"}} --</option>
|
<button id="status-button" class="ui button" name="status_id" value="reopen">
|
||||||
{{range .IssueStatusDefs}}
|
<span class="status-button-icon tw-text-green">{{svg "octicon-issue-reopened"}}</span>
|
||||||
{{if not .ClosesIssue}}
|
<span class="status-button-text">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</span>
|
||||||
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}}</option>
|
</button>
|
||||||
|
{{else}}
|
||||||
|
<select name="status_id" class="ui compact dropdown" style="min-width:140px;padding:7px 10px;border:1px solid var(--color-secondary);border-radius:4px;background:var(--color-body);">
|
||||||
|
<option value="">-- {{ctx.Locale.Tr "repo.issues.status"}} --</option>
|
||||||
|
{{range .IssueStatusDefs}}
|
||||||
|
{{if not .ClosesIssue}}
|
||||||
|
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}}</option>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
<option disabled>---</option>
|
||||||
<option disabled>---</option>
|
{{range .IssueStatusDefs}}
|
||||||
{{range .IssueStatusDefs}}
|
{{if .ClosesIssue}}
|
||||||
{{if .ClosesIssue}}
|
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}} (close)</option>
|
||||||
<option value="{{.ID}}" {{if eq .ID $.Issue.StatusID}}selected{{end}}>{{.Name}} (close)</option>
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{if not $.Issue.IsClosed}}
|
||||||
{{if .Issue.IsClosed}}
|
<option value="close">{{ctx.Locale.Tr "repo.issues.close"}}</option>
|
||||||
<option value="reopen">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</option>
|
{{else}}
|
||||||
{{end}}
|
<option value="reopen">{{ctx.Locale.Tr "repo.issues.reopen_issue"}}</option>
|
||||||
</select>
|
{{end}}
|
||||||
|
</select>
|
||||||
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{$btnIconColor := ""}}{{$btnIcon := ""}}{{$btnTextNoComment := ""}}{{$btnTextWithComment := ""}}{{$btnValue := ""}}
|
{{$btnIconColor := ""}}{{$btnIcon := ""}}{{$btnTextNoComment := ""}}{{$btnTextWithComment := ""}}{{$btnValue := ""}}
|
||||||
{{if .Issue.IsClosed}}
|
{{if .Issue.IsClosed}}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
{{template "repo/issue/sidebar/issue_priority" $}}
|
{{template "repo/issue/sidebar/issue_priority" $}}
|
||||||
|
|
||||||
|
{{template "repo/issue/sidebar/issue_type" $}}
|
||||||
|
|
||||||
{{template "repo/issue/sidebar/custom_fields" $}}
|
{{template "repo/issue/sidebar/custom_fields" $}}
|
||||||
|
|
||||||
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
|
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
{{template "base/head" .}}
|
||||||
|
<div role="main" aria-label="{{.Title}}" class="page-content repository security">
|
||||||
|
{{template "repo/header" .}}
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
|
||||||
|
<h2>{{svg "octicon-shield" 20 "tw-mr-2"}}{{ctx.Locale.Tr "repo.security"}}</h2>
|
||||||
|
{{if .Permission.IsAdmin}}
|
||||||
|
<div class="tw-flex tw-gap-2">
|
||||||
|
<form method="post" action="{{.RepoLink}}/security/scan" class="tw-inline">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<button class="ui small primary button" type="submit">{{svg "octicon-sync" 14}} {{ctx.Locale.Tr "repo.settings.security_scan_now"}}</button>
|
||||||
|
</form>
|
||||||
|
<a class="ui small button" href="{{.RepoLink}}/settings/security">{{svg "octicon-gear" 14}} Settings</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .AlertCounts}}
|
||||||
|
<div class="tw-flex tw-gap-3 tw-mb-4">
|
||||||
|
{{range $sev, $count := .AlertCounts}}
|
||||||
|
<div class="ui {{if eq $sev "critical"}}red{{else if eq $sev "high"}}orange{{else if eq $sev "medium"}}yellow{{else if eq $sev "low"}}blue{{else}}grey{{end}} label">
|
||||||
|
{{$sev}}: {{$count}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .SecurityAlerts}}
|
||||||
|
<table class="ui compact table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_severity"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_scanner_type"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_finding"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_file"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_status"}}</th>
|
||||||
|
{{if .Permission.IsAdmin}}<th></th>{{end}}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .SecurityAlerts}}
|
||||||
|
<tr {{if ne .Status "active"}}class="tw-opacity-50"{{end}}>
|
||||||
|
<td>
|
||||||
|
<span class="ui mini {{if eq .Severity "critical"}}red{{else if eq .Severity "high"}}orange{{else if eq .Severity "medium"}}yellow{{else if eq .Severity "low"}}blue{{else}}grey{{end}} label">
|
||||||
|
{{.Severity}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{.Scanner}}</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{.Title}}</strong>
|
||||||
|
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .FilePath}}
|
||||||
|
<a href="{{$.RepoLink}}/src/branch/{{$.BranchName}}/{{.FilePath}}{{if .LineNumber}}#L{{.LineNumber}}{{end}}">
|
||||||
|
<code class="tw-text-xs">{{.FilePath}}{{if .LineNumber}}:{{.LineNumber}}{{end}}</code>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if eq .Status "active"}}
|
||||||
|
<span class="ui mini red label">Active</span>
|
||||||
|
{{else if eq .Status "resolved"}}
|
||||||
|
<span class="ui mini green label">Resolved</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="ui mini grey label">Dismissed</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
{{if $.Permission.IsAdmin}}
|
||||||
|
<td class="tw-text-right">
|
||||||
|
{{if eq .Status "active"}}
|
||||||
|
<form method="post" action="{{$.RepoLink}}/security/alert/{{.ID}}" class="tw-inline">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="status" value="resolved">
|
||||||
|
<button class="ui tiny green icon button" type="submit" title="Resolve">{{svg "octicon-check" 14}}</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="{{$.RepoLink}}/security/alert/{{.ID}}" class="tw-inline">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="status" value="dismissed">
|
||||||
|
<button class="ui tiny grey icon button" type="submit" title="Dismiss">{{svg "octicon-x" 14}}</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
{{end}}
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<div class="ui segment">
|
||||||
|
<div class="empty-placeholder">
|
||||||
|
<p>{{svg "octicon-shield-check" 48}}</p>
|
||||||
|
<p>{{ctx.Locale.Tr "repo.settings.security_no_alerts"}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/footer" .}}
|
||||||
@@ -19,10 +19,6 @@
|
|||||||
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
|
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_description"}}</label>
|
|
||||||
<input name="description" value="{{.Manifest.Description}}" placeholder="Project description">
|
|
||||||
</div>
|
|
||||||
<div class="three fields">
|
<div class="three fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label>
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
|
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
|
||||||
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
|
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
|
||||||
</a>
|
</a>
|
||||||
|
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{.RepoLink}}/settings/security">
|
||||||
|
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.settings.security"}}
|
||||||
|
</a>
|
||||||
{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
|
{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
|
||||||
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
|
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
|
||||||
{{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}}
|
{{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings security")}}
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{svg "octicon-shield" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.settings.security"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<p class="text grey">{{ctx.Locale.Tr "repo.settings.security_desc"}}</p>
|
||||||
|
|
||||||
|
{{if .AlertCounts}}
|
||||||
|
<div class="tw-flex tw-gap-3 tw-mb-4">
|
||||||
|
{{range $sev, $count := .AlertCounts}}
|
||||||
|
<div class="ui mini {{if eq $sev "critical"}}red{{else if eq $sev "high"}}orange{{else if eq $sev "medium"}}yellow{{else if eq $sev "low"}}blue{{else}}grey{{end}} label">
|
||||||
|
{{$sev}}: {{$count}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<form class="ui form" method="post" action="{{.RepoLink}}/settings/security">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.security_scanners"}}</h5>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="enabled" type="checkbox" {{if .ScannerConfig.Enabled}}checked{{end}}>
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.security_enabled"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="secret_scanner" type="checkbox" {{if .ScannerConfig.SecretScanner}}checked{{end}}>
|
||||||
|
<label>{{svg "octicon-key" 14}} {{ctx.Locale.Tr "repo.settings.security_secret_scanner"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="depend_scanner" type="checkbox" {{if .ScannerConfig.DependScanner}}checked{{end}}>
|
||||||
|
<label>{{svg "octicon-package" 14}} {{ctx.Locale.Tr "repo.settings.security_depend_scanner"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="code_scanner" type="checkbox" {{if .ScannerConfig.CodeScanner}}checked{{end}}>
|
||||||
|
<label>{{svg "octicon-code" 14}} {{ctx.Locale.Tr "repo.settings.security_code_scanner"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="config_scanner" type="checkbox" {{if .ScannerConfig.ConfigScanner}}checked{{end}}>
|
||||||
|
<label>{{svg "octicon-gear" 14}} {{ctx.Locale.Tr "repo.settings.security_config_scanner"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="license_scanner" type="checkbox" {{if .ScannerConfig.LicenseScanner}}checked{{end}}>
|
||||||
|
<label>{{svg "octicon-law" 14}} {{ctx.Locale.Tr "repo.settings.security_license_scanner"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="block_on_push" type="checkbox" {{if .ScannerConfig.BlockOnPush}}checked{{end}}>
|
||||||
|
<label>{{ctx.Locale.Tr "repo.settings.security_block_on_push"}}</label>
|
||||||
|
</div>
|
||||||
|
<p class="help">{{ctx.Locale.Tr "repo.settings.security_block_on_push_help"}}</p>
|
||||||
|
</div>
|
||||||
|
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.settings.security_save"}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="ui top attached header tw-mt-4">
|
||||||
|
{{ctx.Locale.Tr "repo.settings.security_alerts"}}
|
||||||
|
<form method="post" action="{{.RepoLink}}/settings/security/scan" class="tw-float-right tw-inline">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<button class="ui mini primary button" type="submit">{{svg "octicon-sync" 14}} {{ctx.Locale.Tr "repo.settings.security_scan_now"}}</button>
|
||||||
|
</form>
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
{{if .SecurityAlerts}}
|
||||||
|
<table class="ui compact table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_severity"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_scanner_type"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_finding"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_file"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.security_status"}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .SecurityAlerts}}
|
||||||
|
<tr {{if ne .Status "active"}}class="tw-opacity-50"{{end}}>
|
||||||
|
<td>
|
||||||
|
<span class="ui mini {{if eq .Severity "critical"}}red{{else if eq .Severity "high"}}orange{{else if eq .Severity "medium"}}yellow{{else if eq .Severity "low"}}blue{{else}}grey{{end}} label">
|
||||||
|
{{.Severity}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{.Scanner}}</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{.Title}}</strong>
|
||||||
|
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if .FilePath}}
|
||||||
|
<code class="tw-text-xs">{{.FilePath}}{{if .LineNumber}}:{{.LineNumber}}{{end}}</code>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if eq .Status "active"}}
|
||||||
|
<span class="ui mini red label">Active</span>
|
||||||
|
{{else if eq .Status "resolved"}}
|
||||||
|
<span class="ui mini green label">Resolved</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="ui mini grey label">Dismissed</span>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
<td class="tw-text-right">
|
||||||
|
{{if eq .Status "active"}}
|
||||||
|
<form method="post" action="{{$.RepoLink}}/settings/security/alert/{{.ID}}" class="tw-inline">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="status" value="resolved">
|
||||||
|
<button class="ui tiny green icon button" type="submit" title="Resolve">{{svg "octicon-check" 14}}</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="{{$.RepoLink}}/settings/security/alert/{{.ID}}" class="tw-inline">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="status" value="dismissed">
|
||||||
|
<button class="ui tiny grey icon button" type="submit" title="Dismiss">{{svg "octicon-x" 14}}</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-placeholder">
|
||||||
|
<p>{{ctx.Locale.Tr "repo.settings.security_no_alerts"}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{template "repo/settings/layout_footer" .}}
|
||||||
@@ -55,12 +55,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .WikiBreadcrumbs}}
|
||||||
|
{{if gt (len .WikiBreadcrumbs) 1}}
|
||||||
|
<div class="tw-mb-2">
|
||||||
|
<span class="breadcrumb">
|
||||||
|
<a class="section" href="{{.RepoLink}}/wiki/">{{svg "octicon-book" 14}} Wiki</a>
|
||||||
|
{{range .WikiBreadcrumbs}}
|
||||||
|
<span class="breadcrumb-divider">/</span>
|
||||||
|
<a class="section" href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
|
||||||
|
{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .FormatWarning}}
|
{{if .FormatWarning}}
|
||||||
<div class="ui negative message">
|
<div class="ui negative message">
|
||||||
<p>{{.FormatWarning}}</p>
|
<p>{{.FormatWarning}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if .IsWikiFolder}}
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{svg "octicon-file-directory" 16 "tw-mr-2"}}{{.WikiFolderPath}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
{{if .WikiFolderEntries}}
|
||||||
|
<div class="wiki-folder-listing">
|
||||||
|
{{range .WikiFolderEntries}}
|
||||||
|
<div class="tw-py-1">
|
||||||
|
{{if (StringUtils.HasSuffix .Name "/")}}
|
||||||
|
{{svg "octicon-file-directory" 16 "tw-mr-1"}}
|
||||||
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
||||||
|
{{else}}
|
||||||
|
{{svg "octicon-file" 16 "tw-mr-1"}}
|
||||||
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}">{{.Name}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p class="text grey">This folder is empty.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<div class="wiki-content-parts">
|
<div class="wiki-content-parts">
|
||||||
{{if .WikiSidebarTocHTML}}
|
{{if .WikiSidebarTocHTML}}
|
||||||
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
|
<div class="render-content markup wiki-content-sidebar wiki-content-toc">
|
||||||
@@ -68,11 +107,45 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}">
|
<div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML .WikiTree}}with-sidebar{{end}}">
|
||||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
||||||
{{.WikiContentHTML}}
|
{{.WikiContentHTML}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if .WikiTree}}
|
||||||
|
<div class="render-content markup wiki-content-sidebar wiki-content-tree">
|
||||||
|
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
|
||||||
|
<ul class="wiki-tree-list">
|
||||||
|
{{range .WikiTree}}
|
||||||
|
<li>
|
||||||
|
{{if .IsDir}}
|
||||||
|
{{svg "octicon-file-directory" 14}}
|
||||||
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
||||||
|
{{if .Children}}
|
||||||
|
<ul>
|
||||||
|
{{range .Children}}
|
||||||
|
<li>
|
||||||
|
{{if .IsDir}}
|
||||||
|
{{svg "octicon-file-directory" 14}}
|
||||||
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}"><strong>{{.Name}}</strong></a>
|
||||||
|
{{else}}
|
||||||
|
{{svg "octicon-file" 14}}
|
||||||
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
{{svg "octicon-file" 14}}
|
||||||
|
<a href="{{$.RepoLink}}/wiki/{{.SubURL}}" {{if eq $.PageURL .SubURL}}class="active"{{end}}>{{.Name}}</a>
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .WikiSidebarHTML}}
|
{{if .WikiSidebarHTML}}
|
||||||
<div class="render-content markup wiki-content-sidebar">
|
<div class="render-content markup wiki-content-sidebar">
|
||||||
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@
|
|||||||
{{range .Labels}}
|
{{range .Labels}}
|
||||||
<a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.RenderUtils.RenderLabel .}}</a>
|
<a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.RenderUtils.RenderLabel .}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if and .TypeID $.IssueTypeDefs}}{{range $.IssueTypeDefs}}{{if eq .ID $.TypeID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
|
||||||
|
{{if and .PriorityID $.IssuePriorityDefs}}{{range $.IssuePriorityDefs}}{{if eq .ID $.PriorityID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
|
||||||
|
{{if and .StatusID $.IssueStatusDefs}}{{range $.IssueStatusDefs}}{{if eq .ID $.StatusID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .TotalTrackedTime}}
|
{{if .TotalTrackedTime}}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
|
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
|
||||||
</form>
|
</form>
|
||||||
|
{{if or .OAuth2Providers .EnableSSPI}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{template "user/auth/external_auth_methods" .}}
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,27 @@
|
|||||||
<a class="tw-block tw-my-4" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>
|
<a class="tw-block tw-my-4" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
{{if not .IsSigned}}
|
||||||
|
<div class="tw-max-w-sm tw-mx-auto tw-mt-4">
|
||||||
|
<form class="ui form" action="{{AppSubUrl}}/user/login" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="redirect_to" value="{{.CurrentURL}}">
|
||||||
|
<div class="required field">
|
||||||
|
<label>{{ctx.Locale.Tr "home.uname_holder"}}</label>
|
||||||
|
<input type="text" name="user_name" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="required field">
|
||||||
|
<label>{{ctx.Locale.Tr "password"}}</label>
|
||||||
|
<input type="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
|
||||||
|
</form>
|
||||||
|
{{if or .OAuth2Providers .EnableSSPI}}
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{template "user/auth/external_auth_methods" .}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,29 +9,29 @@
|
|||||||
<div class="ui secondary vertical filter menu tw-bg-transparent">
|
<div class="ui secondary vertical filter menu tw-bg-transparent">
|
||||||
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}">
|
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}">
|
||||||
{{ctx.Locale.Tr "home.issues.in_your_repos"}}
|
{{ctx.Locale.Tr "home.issues.in_your_repos"}}
|
||||||
<strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
|
<span class="ui small label">{{CountFmt .IssueStats.YourRepositoriesCount}}</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "assigned"}}">
|
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "assigned"}}">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
|
{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
|
||||||
<strong>{{CountFmt .IssueStats.AssignCount}}</strong>
|
<span class="ui small label">{{CountFmt .IssueStats.AssignCount}}</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "created_by"}}">
|
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "created_by"}}">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
|
{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
|
||||||
<strong>{{CountFmt .IssueStats.CreateCount}}</strong>
|
<span class="ui small label">{{CountFmt .IssueStats.CreateCount}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{if .PageIsPulls}}
|
{{if .PageIsPulls}}
|
||||||
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "review_requested"}}">
|
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "review_requested"}}">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
|
{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
|
||||||
<strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
|
<span class="ui small label">{{CountFmt .IssueStats.ReviewRequestedCount}}</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "reviewed_by"}}">
|
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "reviewed_by"}}">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
|
{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
|
||||||
<strong>{{CountFmt .IssueStats.ReviewedCount}}</strong>
|
<span class="ui small label">{{CountFmt .IssueStats.ReviewedCount}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "mentioned"}}">
|
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "mentioned"}}">
|
||||||
{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
|
{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
|
||||||
<strong>{{CountFmt .IssueStats.MentionCount}}</strong>
|
<span class="ui small label">{{CountFmt .IssueStats.MentionCount}}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,33 @@
|
|||||||
border-left-style: dashed;
|
border-left-style: dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repository.wiki .wiki-tree-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.5em 0 0 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository.wiki .wiki-tree-list ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 0 0 1.2em;
|
||||||
|
margin: 0;
|
||||||
|
border-left: 1px dashed var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository.wiki .wiki-tree-list li {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository.wiki .wiki-tree-list a.active {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository.wiki .wiki-folder-listing {
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.repository.wiki .wiki-content-main.with-sidebar,
|
.repository.wiki .wiki-content-main.with-sidebar,
|
||||||
.repository.wiki .wiki-content-sidebar {
|
.repository.wiki .wiki-content-sidebar {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Custom Branding
|
||||||
|
|
||||||
|
## Logo & Favicon
|
||||||
|
|
||||||
|
Located in the container at `/var/lib/gitea/custom/public/assets/img/`:
|
||||||
|
- `logo.svg` — Navbar logo
|
||||||
|
- `logo.png` — Fallback logo
|
||||||
|
- `favicon.png` — Browser tab icon
|
||||||
|
- `favicon.svg` — SVG favicon
|
||||||
|
|
||||||
|
Source: Moko Consulting CRM favicon
|
||||||
|
|
||||||
|
## Landing Page
|
||||||
|
|
||||||
|
- `LANDING_PAGE = organizations` in `app.ini`
|
||||||
|
- Custom JS redirects home/logo clicks to `/explore/organizations`
|
||||||
|
- After login, users see the organizations list
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
Custom CSS themes at `/var/lib/gitea/custom/public/assets/css/`:
|
||||||
|
- `theme-moko-dark.css`
|
||||||
|
- `theme-moko-light.css`
|
||||||
|
- `theme-moko-auto.css`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
|
| Revision | Date | Author | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
|
||||||
@@ -28,23 +28,42 @@ Each status has:
|
|||||||
| Sort Order | Controls display order in dropdowns (ascending) |
|
| Sort Order | Controls display order in dropdowns (ascending) |
|
||||||
| Is Active | Inactive statuses are hidden from dropdowns but preserved on existing issues |
|
| Is Active | Inactive statuses are hidden from dropdowns but preserved on existing issues |
|
||||||
|
|
||||||
### Example Statuses
|
### Default Statuses (auto-seeded)
|
||||||
|
|
||||||
| Status | Color | Closes Issue | Use Case |
|
| Status | Color | Closes Issue | Use Case |
|
||||||
|--------|-------|:------------:|----------|
|
|--------|-------|:------------:|----------|
|
||||||
| In Progress | Blue | No | Work is actively being done |
|
| In Progress | Blue | No | Work is actively being done |
|
||||||
| Needs Info | Yellow | No | Waiting for more information from reporter |
|
| Needs Info | Yellow | No | Waiting for more information |
|
||||||
| Blocked | Red | No | Cannot proceed due to external dependency |
|
| Blocked | Red | No | Cannot proceed due to dependency |
|
||||||
| Won't Fix | Gray | Yes | Decided not to address this issue |
|
| Resolved | Green | Yes | Fix implemented and verified |
|
||||||
| Duplicate | Purple | Yes | Already tracked in another issue |
|
| Won't Fix | Gray | Yes | Decided not to address |
|
||||||
| Resolved | Green | Yes | Fix has been implemented and verified |
|
| Duplicate | Purple | Yes | Already tracked elsewhere |
|
||||||
|
| Pending: Design | Lavender | No | Waiting on design work |
|
||||||
|
| Pending: Testing | Yellow | No | Waiting for testing |
|
||||||
|
| Pending: Review | Green | No | Waiting for code review |
|
||||||
|
| Pending: Feedback | Pink | No | Waiting for feedback |
|
||||||
|
| Pending: Documentation | Purple | No | Waiting for docs |
|
||||||
|
| Pending: Deployment | Blue | No | Ready to deploy |
|
||||||
|
| Pending: Dependency | Light Blue | No | Blocked by external dependency |
|
||||||
|
|
||||||
|
Statuses are auto-seeded when an org first accesses them. Admins can add, edit, reorder, or deactivate statuses.
|
||||||
|
|
||||||
|
## Comment Form Integration
|
||||||
|
|
||||||
|
The status dropdown **replaces the close/reopen button** in the comment form footer for issues with org statuses:
|
||||||
|
- Open issues show all statuses plus a "Close" option
|
||||||
|
- Selecting a status with `closes_issue = true` auto-closes the issue
|
||||||
|
- Closed issues show only "Reopen" for non-admin users
|
||||||
|
- Admins see the full dropdown on closed issues including "Reopen"
|
||||||
|
- PRs still use the standard close/reopen button
|
||||||
|
|
||||||
|
## Issue List Badges
|
||||||
|
|
||||||
|
Status shows as a colored badge on each issue in the issue list view, alongside Type and Priority badges.
|
||||||
|
|
||||||
## Issue Sidebar
|
## Issue Sidebar
|
||||||
|
|
||||||
When an organization has custom statuses defined, a **Status** dropdown appears in the issue sidebar between labels and custom fields. The dropdown:
|
Status also appears as a read-only display in the sidebar (the editable control is in the comment form). The dropdown:
|
||||||
|
|
||||||
- Shows all active status definitions for the repo's organization
|
|
||||||
- Auto-submits on change (no save button needed)
|
|
||||||
- Displays a colored left border on each option
|
- Displays a colored left border on each option
|
||||||
- Shows a power symbol on statuses that close the issue
|
- Shows a power symbol on statuses that close the issue
|
||||||
- Selecting "—" (empty) clears the status
|
- Selecting "—" (empty) clears the status
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
## Docker Image
|
||||||
|
|
||||||
|
MokoGitea runs as a custom Docker image built from the `moko/1.25.5-project-api` branch.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```bash
|
||||||
|
cd /opt/MokoGitea
|
||||||
|
git pull
|
||||||
|
docker build -t mokogitea:1.25.5-project-api -f Dockerfile.rootless .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
The docker-compose at `/opt/gitea/docker-compose.yml` references the image:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
gitea:
|
||||||
|
image: mokogitea:1.25.5-project-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Process
|
||||||
|
1. Pull latest from `moko/1.25.5-project-api`
|
||||||
|
2. Rebuild Docker image
|
||||||
|
3. `docker compose down gitea && docker compose up -d gitea`
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `./gitea/data` | Repository data, LFS, avatars |
|
||||||
|
| `./gitea/conf` | `app.ini` configuration |
|
||||||
|
|
||||||
|
## Custom Files
|
||||||
|
|
||||||
|
Located at `/var/lib/gitea/custom/`:
|
||||||
|
- `templates/custom/header.tmpl` — Branding, logo redirect
|
||||||
|
- `public/assets/img/logo.svg` — Moko logo
|
||||||
|
- `public/assets/img/favicon.png` — Moko favicon
|
||||||
|
- `public/assets/css/theme-moko-*.css` — Custom themes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
|
| Revision | Date | Author | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
|
||||||
+7
-3
@@ -7,7 +7,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
|
|||||||
| **Language** | Go |
|
| **Language** | Go |
|
||||||
| **License** | MIT |
|
| **License** | MIT |
|
||||||
| **Upstream** | Gitea 1.26.1 |
|
| **Upstream** | Gitea 1.26.1 |
|
||||||
| **Version** | v1.26.1-moko.06.04.00 |
|
| **Version** | v1.26.1-moko.06.11.01 |
|
||||||
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) |
|
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -16,10 +16,13 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
|
|||||||
|
|
||||||
- **Commercial License System** — Package-based license keys with download gating, domain restriction, key expiry, and payment webhook API
|
- **Commercial License System** — Package-based license keys with download gating, domain restriction, key expiry, and payment webhook API
|
||||||
- **Update Server** — Built-in update feeds for Joomla, WordPress, Dolibarr, Composer, Drupal, PrestaShop, and WHMCS
|
- **Update Server** — Built-in update feeds for Joomla, WordPress, Dolibarr, Composer, Drupal, PrestaShop, and WHMCS
|
||||||
- **Custom Issue Statuses** — Org-defined workflow states (In Progress, Blocked, Won't Fix) with auto close/reopen
|
- **First-Class Issue Fields** — Type (12 types), Status (13 statuses), Priority (4 levels) as built-in fields with auto-seed defaults, colored badges in issue list, and sidebar dropdowns
|
||||||
|
- **Security Scanning** — Built-in security scanner with secret detection (15 patterns), push-time scanning, alert management, and Security tab in repo navigation
|
||||||
- **Custom Fields** — Org-level field definitions for issues (sidebar) and repos (metadata) with dropdown, text, number, date, checkbox, and URL types
|
- **Custom Fields** — Org-level field definitions for issues (sidebar) and repos (metadata) with dropdown, text, number, date, checkbox, and URL types
|
||||||
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API for CI/CD integration
|
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API and auto-sync on push
|
||||||
|
- **Wiki Folders** — Hierarchical folder navigation with sidebar tree, breadcrumbs, and index page fallback
|
||||||
- **Well-Known File Tabs** — README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG tabs on repo home page
|
- **Well-Known File Tabs** — README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG tabs on repo home page
|
||||||
|
- **MCP Server** — 120+ tool MCP server published to npm (@mokoconsulting/mokogitea-mcp) with SSE transport
|
||||||
- **Org-Level Branch Protection** — Organization-scoped rulesets that cascade to all repos. Supports glob patterns. Full CRUD API
|
- **Org-Level Branch Protection** — Organization-scoped rulesets that cascade to all repos. Supports glob patterns. Full CRUD API
|
||||||
- **Enterprise Sub-Orgs** — Parent-child organization hierarchy
|
- **Enterprise Sub-Orgs** — Parent-child organization hierarchy
|
||||||
- **Three-Level Visibility** — Public (200), Private (403), Hidden (404) for repositories
|
- **Three-Level Visibility** — Public (200), Private (403), Hidden (404) for repositories
|
||||||
@@ -39,6 +42,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
|
|||||||
| [Org Branch Protection API](Org-Branch-Protection-API) | Org-level branch protection rulesets and API reference |
|
| [Org Branch Protection API](Org-Branch-Protection-API) | Org-level branch protection rulesets and API reference |
|
||||||
| [Project API](Project-API) | Custom API endpoint reference for project boards |
|
| [Project API](Project-API) | Custom API endpoint reference for project boards |
|
||||||
| [Roadmap](Roadmap) | Development roadmap and planned features |
|
| [Roadmap](Roadmap) | Development roadmap and planned features |
|
||||||
|
| [features/](features) | Feature documentation folder |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Org-Level Branch Protection API
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
MokoGitea v1261.0.0 introduces **organization-level branch protection rulesets** that cascade automatically to all repositories within an organization. This eliminates the need to configure identical branch protection rules on each repo individually.
|
||||||
|
|
||||||
|
## How Inheritance Works
|
||||||
|
|
||||||
|
1. **Repo rules take precedence** — If a repo has its own protection rule for a branch pattern (e.g., `main`), the org rule is ignored for that repo.
|
||||||
|
2. **Org rules are the fallback** — If no repo-level rule matches a branch, the system checks org-level rules.
|
||||||
|
3. **Team-based only** — Org rules reference teams, not individual users (use repo-level rules for per-user whitelists).
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
All endpoints require authentication (`token`) and org ownership permissions.
|
||||||
|
|
||||||
|
### List Rules
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/orgs/{org}/branch_protections
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Rule
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/orgs/{org}/branch_protections
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rule_name": "main",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_teams": ["developers"],
|
||||||
|
"enable_merge_whitelist": true,
|
||||||
|
"merge_whitelist_teams": ["maintainers"],
|
||||||
|
"required_approvals": 2,
|
||||||
|
"block_on_rejected_reviews": true,
|
||||||
|
"block_on_outdated_branch": true,
|
||||||
|
"dismiss_stale_approvals": true,
|
||||||
|
"require_signed_commits": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Rule
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/orgs/{org}/branch_protections/{name}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Rule
|
||||||
|
|
||||||
|
```
|
||||||
|
PATCH /api/v1/orgs/{org}/branch_protections/{name}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only fields included in the request body are updated.
|
||||||
|
|
||||||
|
### Delete Rule
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/v1/orgs/{org}/branch_protections/{name}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Glob Patterns
|
||||||
|
|
||||||
|
Rule names support glob patterns for matching multiple branches:
|
||||||
|
|
||||||
|
| Pattern | Matches |
|
||||||
|
|---------|---------|
|
||||||
|
| `main` | Exactly `main` |
|
||||||
|
| `dev` | Exactly `dev` |
|
||||||
|
| `rc/*` | `rc/1.0`, `rc/2.0-beta`, etc. |
|
||||||
|
| `beta/*` | `beta/feature-x`, etc. |
|
||||||
|
| `release/**` | `release/v1`, `release/v1/hotfix`, etc. |
|
||||||
|
|
||||||
|
## Example: Protect All Standard Branches
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN="your-token"
|
||||||
|
ORG="MokoConsulting"
|
||||||
|
API="https://git.mokoconsulting.tech/api/v1"
|
||||||
|
|
||||||
|
for BRANCH in main dev "rc/*" "beta/*" "alpha/*"; do
|
||||||
|
curl -X POST "$API/orgs/$ORG/branch_protections" \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"rule_name\": \"$BRANCH\",
|
||||||
|
\"enable_push\": true,
|
||||||
|
\"enable_push_whitelist\": true,
|
||||||
|
\"push_whitelist_teams\": [\"developers\"],
|
||||||
|
\"required_approvals\": 1,
|
||||||
|
\"block_on_rejected_reviews\": true,
|
||||||
|
\"block_on_outdated_branch\": true
|
||||||
|
}"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration: Help & Support URLs
|
||||||
|
|
||||||
|
Also new in v1261.0.0 — configurable help/support links in `app.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[DEFAULT]
|
||||||
|
HELP_URL = https://docs.mokoconsulting.tech
|
||||||
|
SUPPORT_URL = https://mokoconsulting.tech/support
|
||||||
|
```
|
||||||
|
|
||||||
|
These replace the hardcoded `docs.gitea.com` links in the navigation bar and are visible in **Site Admin > Configuration**.
|
||||||
|
|
||||||
|
## Version Convention
|
||||||
|
|
||||||
|
MokoGitea uses `1261.xx.xx` versioning where `1261` represents the fork starting point from upstream Gitea. Minor and patch numbers track MokoGitea-specific releases.
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
# Project Board API Reference
|
||||||
|
|
||||||
|
Complete REST API for managing Gitea project boards, columns, and issue cards. This API was added by MokoGitea and is not available in upstream Gitea.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All write endpoints require a token with `issue` scope:
|
||||||
|
```
|
||||||
|
Authorization: token YOUR_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
### List Projects
|
||||||
|
```
|
||||||
|
GET /api/v1/repos/{owner}/{repo}/projects
|
||||||
|
```
|
||||||
|
Query parameters:
|
||||||
|
- `state` — `open` (default), `closed`, or `all`
|
||||||
|
- `page` — page number (1-based)
|
||||||
|
- `limit` — results per page
|
||||||
|
|
||||||
|
Response: Array of Project objects
|
||||||
|
|
||||||
|
### Create Project
|
||||||
|
```
|
||||||
|
POST /api/v1/repos/{owner}/{repo}/projects
|
||||||
|
```
|
||||||
|
Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Sprint Q2 2026",
|
||||||
|
"description": "Second quarter sprint",
|
||||||
|
"board_type": 1,
|
||||||
|
"card_type": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `board_type`: 0=none, 1=basic kanban, 2=bug triage
|
||||||
|
- `card_type`: 0=text only, 1=images and text
|
||||||
|
|
||||||
|
### Get Project
|
||||||
|
```
|
||||||
|
GET /api/v1/repos/{owner}/{repo}/projects/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Project
|
||||||
|
```
|
||||||
|
PATCH /api/v1/repos/{owner}/{repo}/projects/{id}
|
||||||
|
```
|
||||||
|
Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Updated Title",
|
||||||
|
"description": "Updated description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Project
|
||||||
|
```
|
||||||
|
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Close/Reopen Project
|
||||||
|
```
|
||||||
|
POST /api/v1/repos/{owner}/{repo}/projects/{id}/close
|
||||||
|
POST /api/v1/repos/{owner}/{repo}/projects/{id}/reopen
|
||||||
|
```
|
||||||
|
|
||||||
|
## Columns
|
||||||
|
|
||||||
|
### List Columns
|
||||||
|
```
|
||||||
|
GET /api/v1/repos/{owner}/{repo}/projects/{id}/columns
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Column
|
||||||
|
```
|
||||||
|
POST /api/v1/repos/{owner}/{repo}/projects/{id}/columns
|
||||||
|
```
|
||||||
|
Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Backlog",
|
||||||
|
"color": "#0075ca"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Column
|
||||||
|
```
|
||||||
|
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Issue Cards
|
||||||
|
|
||||||
|
### List Issues in Column
|
||||||
|
```
|
||||||
|
GET /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues
|
||||||
|
```
|
||||||
|
|
||||||
|
Response: Array of ProjectColumnIssue objects with `issue_id`, `project_id`, `column_id`, `sorting`
|
||||||
|
|
||||||
|
### Add Issue to Column
|
||||||
|
```
|
||||||
|
POST /api/v1/repos/{owner}/{repo}/projects/{id}/columns/{columnId}/issues
|
||||||
|
```
|
||||||
|
Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issue_id": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Move Issue Between Columns
|
||||||
|
```
|
||||||
|
PATCH /api/v1/repos/{owner}/{repo}/projects/{id}/issues/{issueId}/move
|
||||||
|
```
|
||||||
|
Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"column_id": 5,
|
||||||
|
"sorting": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove Issue from Project
|
||||||
|
```
|
||||||
|
DELETE /api/v1/repos/{owner}/{repo}/projects/{id}/issues/{issueId}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Types
|
||||||
|
|
||||||
|
### Project
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Roadmap",
|
||||||
|
"description": "Development roadmap",
|
||||||
|
"owner_id": 2,
|
||||||
|
"repo_id": 68,
|
||||||
|
"creator_id": 1,
|
||||||
|
"is_closed": false,
|
||||||
|
"created_at": "2026-05-08T00:06:45Z",
|
||||||
|
"updated_at": "2026-05-08T00:06:45Z",
|
||||||
|
"closed_at": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ProjectColumn
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"title": "Backlog",
|
||||||
|
"sorting": 0,
|
||||||
|
"color": "#0075ca",
|
||||||
|
"project_id": 1,
|
||||||
|
"default": false,
|
||||||
|
"created_at": "2026-05-08T00:06:58Z",
|
||||||
|
"updated_at": "2026-05-08T00:06:58Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ProjectColumnIssue
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"issue_id": 42,
|
||||||
|
"project_id": 1,
|
||||||
|
"column_id": 7,
|
||||||
|
"sorting": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Integration
|
||||||
|
|
||||||
|
The `project-mcp` server wraps this API. Key tool: `project_setup_roadmap` creates a full project board with columns and loads all open issues in one call.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a project
|
||||||
|
curl -X POST -H "Authorization: token TOKEN" \
|
||||||
|
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects \
|
||||||
|
-d '{"title":"Roadmap","board_type":1}'
|
||||||
|
|
||||||
|
# Add columns
|
||||||
|
curl -X POST -H "Authorization: token TOKEN" \
|
||||||
|
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects/1/columns \
|
||||||
|
-d '{"title":"Backlog"}'
|
||||||
|
|
||||||
|
# Add an issue
|
||||||
|
curl -X POST -H "Authorization: token TOKEN" \
|
||||||
|
https://git.mokoconsulting.tech/api/v1/repos/MokoConsulting/MokoCRM/projects/1/columns/1/issues \
|
||||||
|
-d '{"issue_id":42}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Repo: [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
|
| Revision | Date | Author | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1.0 | 2026-05-09 | Moko Consulting | Initial version |
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# MokoGitea Roadmap
|
||||||
|
|
||||||
|
## Recently Completed (v1.26.1-moko.06.11)
|
||||||
|
|
||||||
|
- First-class Type field (12 types) replacing labels and custom fields
|
||||||
|
- First-class Status field (13 statuses) with auto close/reopen
|
||||||
|
- First-class Priority field (4 levels) with auto-seed defaults
|
||||||
|
- All org labels migrated to first-class fields and deleted
|
||||||
|
- Type/Status/Priority colored badges in issue list view
|
||||||
|
- Security scanning platform with 15 secret detection patterns
|
||||||
|
- Security tab in repo navigation (admin-only)
|
||||||
|
- Wiki hierarchical folder navigation with sidebar tree
|
||||||
|
- Well-known file tabs (README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG)
|
||||||
|
- Repo manifest settings with REST API and auto-sync on push
|
||||||
|
- MCP server published to npm (@mokoconsulting/mokogitea-mcp) with SSE transport
|
||||||
|
- Dashboard issue count badges fixed
|
||||||
|
- Status dropdown replaces close/reopen button
|
||||||
|
- Org settings page for Issue Types
|
||||||
|
- MCP SSE endpoint hosted at git.mokoconsulting.tech/mcp/
|
||||||
|
- npm auto-publish workflow on MCP source changes
|
||||||
|
- OAuth providers on 403/404 error pages
|
||||||
|
- All stale branches cleaned up (main + dev only)
|
||||||
|
|
||||||
|
## In Progress
|
||||||
|
|
||||||
|
- Rename moko-platform to MokoPlatform
|
||||||
|
- Granular role-based permissions for all features (#9)
|
||||||
|
- Wire moko-platform CLI to manifest API (#505)
|
||||||
|
- Bulk migrate remaining 41 flat wikis to folders
|
||||||
|
|
||||||
|
## Planned
|
||||||
|
|
||||||
|
- Standard status presets and cross-org migration (#507)
|
||||||
|
- Auto-create default teams on org creation (#513)
|
||||||
|
- Update server reads from repo_manifest (#512)
|
||||||
|
- Dependency vulnerability scanner module
|
||||||
|
- Code security analysis scanner module
|
||||||
|
- Payment gateways for license keys (#135)
|
||||||
|
- Independent visibility controls for issues/wiki/projects (#133)
|
||||||
|
- MCP SSE endpoint hosted at git.mokoconsulting.tech/mcp
|
||||||
|
- Smithery/Claude Code marketplace listing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| Revision | Date | Author | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 3.0 | 2026-06-06 | Jonathan Miller (@jmiller) | First-class fields, security scanning, wiki folders, MCP release |
|
||||||
|
| 2.0 | 2026-06-06 | Jonathan Miller (@jmiller) | Complete rewrite with current features and priorities |
|
||||||
|
| 1.0 | 2026-05-09 | Jonathan Miller (@jmiller) | Initial version |
|
||||||
Reference in New Issue
Block a user