Add environment variables for GitHub, GitLab, Gitea, and allowed hosts configuration

This commit is contained in:
2026-05-28 19:53:33 +02:00
parent 342490035b
commit 3a42bec2b7
4 changed files with 128 additions and 100 deletions
+18
View File
@@ -1,3 +1,21 @@
APP_ENV=prod APP_ENV=prod
APP_DEBUG=0 APP_DEBUG=0
APP_SECRET=changeme_replace_with_random_32char_string APP_SECRET=changeme_replace_with_random_32char_string
# Comma-separated list of allowed hostnames. Leave empty to allow all hosts.
# Example: ALLOWED_HOSTS=example.com,www.example.com
ALLOWED_HOSTS=
# GitHub
GITHUB_USER=
GITHUB_TOKEN=
# GitLab (leave GITLAB_URL empty to use https://gitlab.com)
GITLAB_USER=
GITLAB_TOKEN=
GITLAB_URL=
# Gitea
GITEA_USER=
GITEA_TOKEN=
GITEA_URL=
+28 -41
View File
@@ -3,7 +3,7 @@
A self-hosted Symfony service that merges contribution data from **GitHub**, **GitLab** and **Gitea** into a single GitHub-style heatmap SVG you can embed anywhere. A self-hosted Symfony service that merges contribution data from **GitHub**, **GitLab** and **Gitea** into a single GitHub-style heatmap SVG you can embed anywhere.
``` ```
https://your-host/graph.svg?github_user=you&github_token=ghp_…&gitea_user=you&gitea_token=…&gitea_url=https://git.example.com https://your-host/graph.svg?theme=dark
``` ```
![Example graph](https://img.shields.io/badge/output-SVG%20heatmap-39d353?style=flat-square) ![Example graph](https://img.shields.io/badge/output-SVG%20heatmap-39d353?style=flat-square)
@@ -35,14 +35,31 @@ cd git-contribution-graph
cp .env .env.local cp .env .env.local
``` ```
Edit `.env.local`: Edit `.env.local` with your credentials:
```dotenv ```dotenv
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=<generate with: openssl rand -hex 16> APP_SECRET=<generate with: openssl rand -hex 16>
# GitHub
GITHUB_USER=your-github-username
GITHUB_TOKEN=ghp_…
# GitLab (omit GITLAB_URL to use gitlab.com)
GITLAB_USER=your-gitlab-username
GITLAB_TOKEN=glpat-…
GITLAB_URL=
# Gitea
GITEA_USER=your-gitea-username
GITEA_TOKEN=…
GITEA_URL=https://git.example.com
# Optional: restrict to specific hostnames (comma-separated), leave empty to allow all
ALLOWED_HOSTS=
``` ```
Only configure the platforms you use — unused ones are silently skipped.
### 2. Start ### 2. Start
```bash ```bash
@@ -65,52 +82,22 @@ GET /health → {"status":"ok"}
| Parameter | Required | Description | | Parameter | Required | Description |
|---|---|---| |---|---|---|
| `github_user` | ✗ | GitHub username |
| `github_token` | ✗ | GitHub PAT — scope: `read:user` |
| `gitlab_user` | ✗ | GitLab username |
| `gitlab_token` | ✗ | GitLab PAT — scope: `read_user`, `read_api` |
| `gitlab_url` | ✗ | GitLab base URL (default: `https://gitlab.com`) |
| `gitea_user` | ✗ | Gitea username |
| `gitea_token` | ✗ | Gitea PAT — scope: `read:user` |
| `gitea_url` | ✗ | Gitea instance URL, e.g. `https://git.example.com` |
| `theme` | ✗ | `dark` (default) or `light` | | `theme` | ✗ | `dark` (default) or `light` |
All platform parameters are optional — include only the ones you use. At least one platform must be configured for a non-empty graph. All credentials are configured via environment variables — see [Deploy](#deploy).
--- ---
## Embedding in a README ## Embedding in a README
### GitHub-only
```markdown ```markdown
![Contribution Graph](https://your-host/graph.svg?github_user=YOUR_USER&github_token=YOUR_TOKEN) <!-- Dark theme (default) -->
![Contribution Graph](https://your-host/graph.svg)
<!-- Light theme -->
![Contribution Graph](https://your-host/graph.svg?theme=light)
``` ```
### GitHub + Gitea
```markdown
![Contribution Graph](https://your-host/graph.svg?github_user=YOUR_GH_USER&github_token=GH_TOKEN&gitea_user=YOUR_GITEA_USER&gitea_token=GITEA_TOKEN&gitea_url=https://git.arthurerlich.de)
```
### All three platforms
```markdown
![Contribution Graph](https://your-host/graph.svg?github_user=GH_USER&github_token=GH_TOKEN&gitlab_user=GL_USER&gitlab_token=GL_TOKEN&gitlab_url=https://gitlab.com&gitea_user=GITEA_USER&gitea_token=GITEA_TOKEN&gitea_url=https://git.arthurerlich.de)
```
### Dark vs Light theme
```markdown
<!-- Dark (default) -->
![Contribution Graph](https://your-host/graph.svg?github_user=USER&github_token=TOKEN&theme=dark)
<!-- Light -->
![Contribution Graph](https://your-host/graph.svg?github_user=USER&github_token=TOKEN&theme=light)
```
> **Security note:** API tokens embedded in public Markdown URLs are visible to anyone. Use read-only fine-grained tokens with minimal scopes. For private repos or sensitive setups, proxy the request server-side and keep tokens in environment variables.
--- ---
## Token setup ## Token setup
@@ -146,7 +133,7 @@ composer install
APP_ENV=dev php -S localhost:8080 -t public APP_ENV=dev php -S localhost:8080 -t public
# Test endpoint # Test endpoint
curl "http://localhost:8080/graph.svg?gitea_user=haylan&gitea_token=YOUR_TOKEN&gitea_url=https://git.arthurerlich.de" -o graph.svg curl "http://localhost:8080/graph.svg" -o graph.svg
``` ```
--- ---
+9
View File
@@ -9,6 +9,15 @@ services:
APP_ENV: prod APP_ENV: prod
APP_DEBUG: "0" APP_DEBUG: "0"
APP_SECRET: "${APP_SECRET}" APP_SECRET: "${APP_SECRET}"
ALLOWED_HOSTS: "${ALLOWED_HOSTS:-}"
GITHUB_USER: "${GITHUB_USER:-}"
GITHUB_TOKEN: "${GITHUB_TOKEN:-}"
GITLAB_USER: "${GITLAB_USER:-}"
GITLAB_TOKEN: "${GITLAB_TOKEN:-}"
GITLAB_URL: "${GITLAB_URL:-}"
GITEA_USER: "${GITEA_USER:-}"
GITEA_TOKEN: "${GITEA_TOKEN:-}"
GITEA_URL: "${GITEA_URL:-}"
volumes: volumes:
- cache:/app/var/cache - cache:/app/var/cache
- logs:/app/var/log - logs:/app/var/log
+73 -59
View File
@@ -7,6 +7,7 @@ use App\Service\GitHubProvider;
use App\Service\GitLabProvider; use App\Service\GitLabProvider;
use App\Service\SvgRenderer; use App\Service\SvgRenderer;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -15,6 +16,9 @@ use Symfony\Contracts\Cache\ItemInterface;
class GraphController class GraphController
{ {
/** @var list<string> */
private readonly array $allowedHosts;
public function __construct( public function __construct(
private readonly GitHubProvider $github, private readonly GitHubProvider $github,
private readonly GitLabProvider $gitlab, private readonly GitLabProvider $gitlab,
@@ -22,76 +26,47 @@ class GraphController
private readonly SvgRenderer $renderer, private readonly SvgRenderer $renderer,
private readonly CacheInterface $cache, private readonly CacheInterface $cache,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
) {} #[Autowire(env: 'ALLOWED_HOSTS')]
string $allowedHosts = '',
#[Autowire(env: 'GITHUB_USER')]
private readonly string $githubUser = '',
#[Autowire(env: 'GITHUB_TOKEN')]
private readonly string $githubToken = '',
#[Autowire(env: 'GITLAB_USER')]
private readonly string $gitlabUser = '',
#[Autowire(env: 'GITLAB_TOKEN')]
private readonly string $gitlabToken = '',
#[Autowire(env: 'GITLAB_URL')]
private readonly string $gitlabUrl = '',
#[Autowire(env: 'GITEA_USER')]
private readonly string $giteaUser = '',
#[Autowire(env: 'GITEA_TOKEN')]
private readonly string $giteaToken = '',
#[Autowire(env: 'GITEA_URL')]
private readonly string $giteaUrl = '',
) {
$this->allowedHosts = array_values(array_filter(array_map('trim', explode(',', $allowedHosts))));
}
/** /**
* Returns a GitHub-style contribution graph SVG merging GitHub, GitLab and Gitea.
*
* Query parameters: * Query parameters:
* github_user string GitHub username
* github_token string GitHub personal access token (needs read:user scope)
* gitlab_user string GitLab username
* gitlab_token string GitLab personal access token (needs read_user scope)
* gitlab_url string GitLab base URL (default: https://gitlab.com)
* gitea_user string Gitea username
* gitea_token string Gitea personal access token
* gitea_url string Gitea instance base URL (e.g. https://git.example.com)
* theme string dark|light (default: dark) * theme string dark|light (default: dark)
*/ */
#[Route('/graph.svg', name: 'contribution_graph', methods: ['GET'])] #[Route('/graph.svg', name: 'contribution_graph', methods: ['GET'])]
public function graph(Request $request): Response public function graph(Request $request): Response
{ {
$params = $request->query->all(); if ($this->allowedHosts !== [] && !in_array($request->getHost(), $this->allowedHosts, true)) {
$theme = $params['theme'] ?? 'dark'; return new Response('Forbidden', 403);
// Use a cache key based on the query params (minus sensitive tokens — we hash them)
$cacheKey = 'graph_' . md5(serialize($params));
$svg = $this->cache->get($cacheKey, function (ItemInterface $item) use ($params, $theme): string {
$item->expiresAfter(3600); // cache for 1 hour
$contributions = [];
if (!empty($params['github_user']) && !empty($params['github_token'])) {
try {
$contributions = $this->merge(
$contributions,
$this->github->fetch($params['github_user'], $params['github_token'])
);
} catch (\Throwable $e) {
$this->logger->warning('GitHub fetch failed: ' . $e->getMessage());
}
} }
if (!empty($params['gitlab_user']) && !empty($params['gitlab_token'])) { $theme = $request->query->get('theme', 'dark');
try {
$contributions = $this->merge(
$contributions,
$this->gitlab->fetch(
$params['gitlab_user'],
$params['gitlab_token'],
$params['gitlab_url'] ?? 'https://gitlab.com'
)
);
} catch (\Throwable $e) {
$this->logger->warning('GitLab fetch failed: ' . $e->getMessage());
}
}
if (!empty($params['gitea_user']) && !empty($params['gitea_token']) && !empty($params['gitea_url'])) { $cacheKey = 'graph_' . $theme;
try {
$contributions = $this->merge( $svg = $this->cache->get($cacheKey, function (ItemInterface $item) use ($theme): string {
$contributions, $item->expiresAfter(3600);
$this->gitea->fetch(
$params['gitea_user'], $contributions = $this->fetchAllContributions();
$params['gitea_token'],
rtrim($params['gitea_url'], '/')
)
);
} catch (\Throwable $e) {
$this->logger->warning('Gitea fetch failed: ' . $e->getMessage());
}
}
return $this->renderer->render($contributions, $theme); return $this->renderer->render($contributions, $theme);
}); });
@@ -108,6 +83,45 @@ class GraphController
return new Response('{"status":"ok"}', 200, ['Content-Type' => 'application/json']); return new Response('{"status":"ok"}', 200, ['Content-Type' => 'application/json']);
} }
private function fetchAllContributions(): array
{
$contributions = [];
if ($this->githubUser !== '' && $this->githubToken !== '') {
try {
$contributions = $this->merge($contributions, $this->github->fetch($this->githubUser, $this->githubToken));
} catch (\Throwable $e) {
$this->logger->warning('GitHub fetch failed: ' . $e->getMessage());
}
}
if ($this->gitlabUser !== '' && $this->gitlabToken !== '') {
try {
$contributions = $this->merge($contributions, $this->gitlab->fetch(
$this->gitlabUser,
$this->gitlabToken,
$this->gitlabUrl !== '' ? $this->gitlabUrl : 'https://gitlab.com'
));
} catch (\Throwable $e) {
$this->logger->warning('GitLab fetch failed: ' . $e->getMessage());
}
}
if ($this->giteaUser !== '' && $this->giteaToken !== '' && $this->giteaUrl !== '') {
try {
$contributions = $this->merge($contributions, $this->gitea->fetch(
$this->giteaUser,
$this->giteaToken,
rtrim($this->giteaUrl, '/')
));
} catch (\Throwable $e) {
$this->logger->warning('Gitea fetch failed: ' . $e->getMessage());
}
}
return $contributions;
}
/** Sum contributions by date from two maps. */ /** Sum contributions by date from two maps. */
private function merge(array $base, array $new): array private function merge(array $base, array $new): array
{ {