first commit
This commit is contained in:
commit
83a871b42c
7 changed files with 542 additions and 0 deletions
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# .gitignore
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
# Contains secrets, should NEVER be committed.
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Python specific
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# Temporary files used by the OIDC Bridge
|
||||||
|
/tmp/*.json
|
||||||
|
|
||||||
|
# IDE / Editor specific
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Dockerfile
|
||||||
|
# Instructions to build the Docker image for our OIDC Bridge application.
|
||||||
|
|
||||||
|
# Start with a lightweight Python base image.
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Set the working directory inside the container.
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the requirements file to install dependencies.
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install the Python packages.
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Create a 'templates' directory for the status page HTML
|
||||||
|
RUN mkdir templates
|
||||||
|
|
||||||
|
# Copy the main application file and the templates.
|
||||||
|
COPY app.py .
|
||||||
|
COPY templates/status.html ./templates/
|
||||||
|
|
||||||
|
# Expose the port the Flask app will run on.
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# The command to run the application when the container starts.
|
||||||
|
CMD ["python", "-u", "app.py"]
|
||||||
95
README.md
Normal file
95
README.md
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# PeerTube Discord OIDC Bridge
|
||||||
|
|
||||||
|
A lightweight, containerized OIDC (OpenID Connect) provider that acts as a bridge between a PeerTube instance and Discord's OAuth2 authentication. This allows users to log in or register on a PeerTube instance using their Discord account, with access gated by membership in a specific Discord server.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This project solves a specific problem: PeerTube has a robust, official plugin for OIDC authentication, but not for generic OAuth2 providers like Discord. This service fills that gap by presenting a fully compliant OIDC interface to PeerTube while handling the Discord OAuth2 flow on the backend.
|
||||||
|
|
||||||
|
The primary use case is for self-hosted PeerTube instances that are not fully public but need a simple way to grant access to a community of users, such as friends or server members, without manual account creation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Discord Authentication:** Enables "Login with Discord" for any PeerTube instance.
|
||||||
|
- **Server Gating:** Restricts login/registration to members of a specific Discord server.
|
||||||
|
- **OIDC Compliant:** Works seamlessly with the official PeerTube `auth-openid-connect` plugin.
|
||||||
|
- **Automatic Account Creation:** New users who pass the server gate are automatically given a PeerTube account.
|
||||||
|
- **Existing Account Linking:** Users with an existing PeerTube account can link it by matching their Discord email.
|
||||||
|
- **Dockerized:** Runs as a single, lightweight Docker container orchestrated with Docker Compose.
|
||||||
|
- **Diagnostics Page:** Includes a `/status` page to check configuration and connectivity.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. A user on PeerTube clicks "Login with OpenID Connect".
|
||||||
|
2. PeerTube redirects the user to this OIDC Bridge service.
|
||||||
|
3. The OIDC Bridge service redirects the user to Discord for authentication and authorization.
|
||||||
|
4. The user authorizes the application in Discord and is redirected back to the bridge.
|
||||||
|
5. The bridge uses the Discord authorization code to get an access token.
|
||||||
|
6. It then uses the access token to fetch the user's profile and their list of Discord servers (guilds).
|
||||||
|
7. The bridge creates a signed **ID Token (JWT)**, inserting the user's server IDs into a `groups` claim.
|
||||||
|
8. PeerTube receives the ID Token, validates it, and checks if the user's `groups` claim contains the required "Allowed Group" (your Discord Server ID).
|
||||||
|
9. If the check passes, the user is logged in or their account is created.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- [Docker](https://www.docker.com/get-started)
|
||||||
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
|
- A PeerTube instance with the `auth-openid-connect` plugin installed and enabled.
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Clone or Create Project Files
|
||||||
|
|
||||||
|
Place all the project files (`docker-compose.yml`, `Dockerfile`, `app.py`, `requirements.txt`, `.env`, `templates/status.html`) in a single directory on your server.
|
||||||
|
|
||||||
|
### 2. Configure Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file by copying the contents from the Canvas and fill in the following values:
|
||||||
|
|
||||||
|
- `OIDC_PROVIDER_URL`: The full, publicly accessible URL of this bridge service (e.g., `http://your-domain.com:5000` or `http://192.168.1.10:5000`). **This cannot be `localhost`** if PeerTube is running on a different machine or in a different Docker network.
|
||||||
|
- `DISCORD_CLIENT_ID`: Your Client ID from the Discord Developer Portal.
|
||||||
|
- `DISCORD_CLIENT_SECRET`: Your Client Secret from the Discord Developer Portal.
|
||||||
|
- `PEERTUBE_CALLBACK_URL`: The full callback URL provided by the PeerTube OIDC plugin settings page. It will look like `https://your-peertube.com/plugins/auth-openid-connect/router/code-cb`.
|
||||||
|
|
||||||
|
### 3. Configure Discord Application
|
||||||
|
|
||||||
|
In the [Discord Developer Portal](https://discord.com/developers/applications), under your application's "OAuth2" settings:
|
||||||
|
|
||||||
|
- Add a **Redirect URI** that matches the bridge's callback endpoint: `http://<your-domain-or-ip>:5000/discord/callback`.
|
||||||
|
|
||||||
|
### 4. Configure PeerTube Plugin
|
||||||
|
|
||||||
|
On your PeerTube instance, navigate to **Administration -> Plugins/Themes -> auth-openid-connect -> Settings** and configure it as follows:
|
||||||
|
|
||||||
|
- **Discover URL:** `http://<your-domain-or-ip>:5000/.well-known/openid-configuration`
|
||||||
|
- **Client ID:** `peertube`
|
||||||
|
- **Client secret:** `peertube-super-secret`
|
||||||
|
- **Scope:** `openid email profile groups`
|
||||||
|
- **Username property:** `preferred_username`
|
||||||
|
- **Email property:** `email`
|
||||||
|
- **Display name property:** `name`
|
||||||
|
- **Group property:** `groups`
|
||||||
|
- **Allowed group:** Your specific Discord Server ID.
|
||||||
|
|
||||||
|
## Running the Service
|
||||||
|
|
||||||
|
From the project directory, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The -d flag runs the container in detached mode (in the background).
|
||||||
|
Troubleshooting
|
||||||
|
|
||||||
|
You can check the health and configuration of the bridge service by navigating to its status page:
|
||||||
|
|
||||||
|
http://<your-domain-or-ip>:5000/status
|
||||||
|
|
||||||
|
This page will show the status of environment variables, connectivity to the Discord API, and a log of the most recent incoming requests from your PeerTube instance, which is invaluable for debugging the connection.
|
||||||
|
|
||||||
|
To view live logs from the container, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
274
app.py
Normal file
274
app.py
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
# app.py
|
||||||
|
# A lightweight OIDC Provider that uses Discord as an upstream identity provider.
|
||||||
|
# This application is designed to work with the PeerTube OpenID Connect plugin.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from collections import deque
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from flask import Flask, request, jsonify, redirect, render_template
|
||||||
|
# This is the corrected import section. We import from 'jose'.
|
||||||
|
from jose import jwt, jwk
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# --- In-memory store for recent requests ---
|
||||||
|
# A deque is a double-ended queue with a fixed size to prevent memory leaks.
|
||||||
|
recent_requests = deque(maxlen=20)
|
||||||
|
|
||||||
|
# --- Configuration from Environment Variables ---
|
||||||
|
# Load all settings from the .env file or system environment.
|
||||||
|
BASE_URL = os.environ.get('OIDC_PROVIDER_URL')
|
||||||
|
DISCORD_CLIENT_ID = os.environ.get('DISCORD_CLIENT_ID')
|
||||||
|
DISCORD_CLIENT_SECRET = os.environ.get('DISCORD_CLIENT_SECRET')
|
||||||
|
OIDC_CLIENT_ID = os.environ.get('OIDC_CLIENT_ID', 'peertube')
|
||||||
|
OIDC_CLIENT_SECRET = os.environ.get('OIDC_CLIENT_SECRET', 'peertube-secret')
|
||||||
|
PEERTUBE_CALLBACK_URL = os.environ.get('PEERTUBE_CALLBACK_URL')
|
||||||
|
|
||||||
|
# This URL must match what's in your Discord Developer Portal settings
|
||||||
|
DISCORD_REDIRECT_URI = f"{BASE_URL}/discord/callback" if BASE_URL else None
|
||||||
|
|
||||||
|
# Generate an RSA key pair for signing ID Tokens in memory.
|
||||||
|
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
public_key = private_key.public_key()
|
||||||
|
|
||||||
|
# --- Logging Helper ---
|
||||||
|
def log_request(endpoint, status, error_message=None):
|
||||||
|
"""Logs an incoming request to our in-memory store."""
|
||||||
|
recent_requests.appendleft({
|
||||||
|
"timestamp": datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC'),
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"ip": request.remote_addr,
|
||||||
|
"status": status,
|
||||||
|
"error": error_message
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- OIDC and JWT Helper Functions ---
|
||||||
|
def get_jwks():
|
||||||
|
"""Generates the JWKS (JSON Web Key Set) required for token validation."""
|
||||||
|
pem = public_key.public_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||||
|
)
|
||||||
|
# Use python-jose to construct the key dictionary
|
||||||
|
key = jwk.construct(pem, algorithm='RS256')
|
||||||
|
|
||||||
|
return {
|
||||||
|
"keys": [{
|
||||||
|
"kty": "RSA",
|
||||||
|
"use": "sig",
|
||||||
|
"kid": "1", # Key ID
|
||||||
|
**key.to_dict()
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_id_token(user_info, discord_guilds):
|
||||||
|
"""Creates a signed ID Token (JWT)."""
|
||||||
|
now = int(time.time())
|
||||||
|
|
||||||
|
claims = {
|
||||||
|
"iss": BASE_URL,
|
||||||
|
"sub": user_info['id'],
|
||||||
|
"aud": OIDC_CLIENT_ID,
|
||||||
|
"exp": now + 3600,
|
||||||
|
"iat": now,
|
||||||
|
"email": user_info['email'],
|
||||||
|
"email_verified": user_info.get('verified', False),
|
||||||
|
"preferred_username": f"{user_info['username']}_{user_info['discriminator']}",
|
||||||
|
"name": user_info.get('global_name') or user_info['username'],
|
||||||
|
"groups": [guild['id'] for guild in discord_guilds]
|
||||||
|
}
|
||||||
|
|
||||||
|
private_pem = private_key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.PKCS8,
|
||||||
|
encryption_algorithm=serialization.NoEncryption()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use jwt.encode from python-jose
|
||||||
|
return jwt.encode(claims, private_pem, algorithm="RS256", headers={"kid": "1"})
|
||||||
|
|
||||||
|
# --- Discord API Interaction ---
|
||||||
|
def exchange_discord_code(code):
|
||||||
|
"""Exchanges a Discord auth code for an access token."""
|
||||||
|
data = {
|
||||||
|
'client_id': DISCORD_CLIENT_ID,
|
||||||
|
'client_secret': DISCORD_CLIENT_SECRET,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
'redirect_uri': DISCORD_REDIRECT_URI
|
||||||
|
}
|
||||||
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
|
r = requests.post('https://discord.com/api/oauth2/token', data=data, headers=headers)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_discord_user_info(access_token):
|
||||||
|
"""Fetches user profile from Discord."""
|
||||||
|
headers = {'Authorization': f'Bearer {access_token}'}
|
||||||
|
r = requests.get('https://discord.com/api/users/@me', headers=headers)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_discord_user_guilds(access_token):
|
||||||
|
"""Fetches the list of servers the user is in."""
|
||||||
|
headers = {'Authorization': f'Bearer {access_token}'}
|
||||||
|
r = requests.get('https://discord.com/api/users/@me/guilds', headers=headers)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
# --- Root and Status Routes ---
|
||||||
|
@app.route('/')
|
||||||
|
def root_redirect():
|
||||||
|
"""Redirects the root URL to the status page for convenience."""
|
||||||
|
return redirect('/status')
|
||||||
|
|
||||||
|
@app.route('/status')
|
||||||
|
def status_endpoint():
|
||||||
|
"""Provides a status page for diagnostics."""
|
||||||
|
# Check environment variables
|
||||||
|
env_checks = {
|
||||||
|
'OIDC_PROVIDER_URL': bool(BASE_URL),
|
||||||
|
'DISCORD_CLIENT_ID': bool(DISCORD_CLIENT_ID),
|
||||||
|
'DISCORD_CLIENT_SECRET': 'Set' if DISCORD_CLIENT_SECRET else 'Not Set',
|
||||||
|
'PEERTUBE_CALLBACK_URL': bool(PEERTUBE_CALLBACK_URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Discord API connectivity
|
||||||
|
discord_api_status = 'Unknown'
|
||||||
|
try:
|
||||||
|
r = requests.get('https://discord.com/api/v10/gateway', timeout=5)
|
||||||
|
if r.status_code == 200:
|
||||||
|
discord_api_status = 'OK'
|
||||||
|
else:
|
||||||
|
discord_api_status = f"Error - Status Code: {r.status_code}"
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
discord_api_status = f"Failed to connect: {e}"
|
||||||
|
|
||||||
|
return render_template('status.html', env_checks=env_checks, discord_api_status=discord_api_status, requests=list(recent_requests))
|
||||||
|
|
||||||
|
|
||||||
|
# --- OIDC Provider Endpoints ---
|
||||||
|
@app.route('/.well-known/openid-configuration')
|
||||||
|
def discovery_endpoint():
|
||||||
|
"""Serves the OIDC discovery document."""
|
||||||
|
log_request('/.well-known/openid-configuration', 'OK')
|
||||||
|
return jsonify({
|
||||||
|
"issuer": BASE_URL,
|
||||||
|
"authorization_endpoint": f"{BASE_URL}/authorize",
|
||||||
|
"token_endpoint": f"{BASE_URL}/token",
|
||||||
|
"userinfo_endpoint": f"{BASE_URL}/userinfo",
|
||||||
|
"jwks_uri": f"{BASE_URL}/jwks.json",
|
||||||
|
"response_types_supported": ["code"],
|
||||||
|
"subject_types_supported": ["public"],
|
||||||
|
"id_token_signing_alg_values_supported": ["RS256"],
|
||||||
|
"scopes_supported": ["openid", "profile", "email", "groups"],
|
||||||
|
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
|
||||||
|
"claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "email_verified", "preferred_username", "name", "groups"]
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/jwks.json')
|
||||||
|
def jwks_endpoint():
|
||||||
|
"""Serves the JSON Web Key Set."""
|
||||||
|
log_request('/jwks.json', 'OK')
|
||||||
|
return jsonify(get_jwks())
|
||||||
|
|
||||||
|
@app.route('/authorize')
|
||||||
|
def authorize_endpoint():
|
||||||
|
"""Starts the login flow, redirecting to Discord."""
|
||||||
|
log_request('/authorize', 'Redirecting to Discord')
|
||||||
|
discord_auth_params = {
|
||||||
|
'client_id': DISCORD_CLIENT_ID,
|
||||||
|
'redirect_uri': DISCORD_REDIRECT_URI,
|
||||||
|
'response_type': 'code',
|
||||||
|
'scope': 'identify email guilds',
|
||||||
|
'state': request.args.get('state')
|
||||||
|
}
|
||||||
|
return redirect(f"https://discord.com/api/oauth2/authorize?{urlencode(discord_auth_params)}")
|
||||||
|
|
||||||
|
@app.route('/discord/callback')
|
||||||
|
def discord_callback_endpoint():
|
||||||
|
"""Handles the callback from Discord and redirects back to PeerTube."""
|
||||||
|
code = request.args.get('code')
|
||||||
|
state = request.args.get('state')
|
||||||
|
if not code:
|
||||||
|
return "Error: Discord callback missing code.", 400
|
||||||
|
return redirect(f"{PEERTUBE_CALLBACK_URL}?code={code}&state={state}")
|
||||||
|
|
||||||
|
@app.route('/token', methods=['POST'])
|
||||||
|
def token_endpoint():
|
||||||
|
"""Exchanges the authorization code for tokens."""
|
||||||
|
if (request.form.get('client_id') != OIDC_CLIENT_ID or
|
||||||
|
request.form.get('client_secret') != OIDC_CLIENT_SECRET):
|
||||||
|
log_request('/token', 'FAIL', 'Invalid client_id or client_secret')
|
||||||
|
return jsonify({"error": "invalid_client"}), 401
|
||||||
|
|
||||||
|
try:
|
||||||
|
code = request.form.get('code')
|
||||||
|
discord_tokens = exchange_discord_code(code)
|
||||||
|
discord_access_token = discord_tokens['access_token']
|
||||||
|
|
||||||
|
user_info = get_discord_user_info(discord_access_token)
|
||||||
|
user_guilds = get_discord_user_guilds(discord_access_token)
|
||||||
|
|
||||||
|
id_token = create_id_token(user_info, user_guilds)
|
||||||
|
access_token = base64.urlsafe_b64encode(os.urandom(32)).decode('utf-8')
|
||||||
|
|
||||||
|
with open(f"/tmp/{access_token}.json", "w") as f:
|
||||||
|
json.dump({**user_info, "groups": [g['id'] for g in user_guilds]}, f)
|
||||||
|
|
||||||
|
log_request('/token', 'OK')
|
||||||
|
return jsonify({
|
||||||
|
'access_token': access_token,
|
||||||
|
'token_type': 'Bearer',
|
||||||
|
'expires_in': 3600,
|
||||||
|
'id_token': id_token,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
log_request('/token', 'FAIL', error_msg)
|
||||||
|
print(f"Error in /token endpoint: {e}")
|
||||||
|
return jsonify({"error": "server_error"}), 500
|
||||||
|
|
||||||
|
@app.route('/userinfo', methods=['GET', 'POST'])
|
||||||
|
def userinfo_endpoint():
|
||||||
|
"""Provides user information to PeerTube."""
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
if not auth_header or not auth_header.startswith('Bearer '):
|
||||||
|
log_request('/userinfo', 'FAIL', 'Missing or malformed Authorization header')
|
||||||
|
return jsonify({"error": "invalid_token"}), 401
|
||||||
|
|
||||||
|
access_token = auth_header.split(' ')[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(f"/tmp/{access_token}.json", "r") as f:
|
||||||
|
user_data = json.load(f)
|
||||||
|
|
||||||
|
log_request('/userinfo', 'OK')
|
||||||
|
return jsonify({
|
||||||
|
"sub": user_data['id'],
|
||||||
|
"email": user_data['email'],
|
||||||
|
"preferred_username": f"{user_data['username']}_{user_data['discriminator']}",
|
||||||
|
"name": user_data.get('global_name') or user_data['username'],
|
||||||
|
"groups": user_data.get('groups', [])
|
||||||
|
})
|
||||||
|
except FileNotFoundError:
|
||||||
|
log_request('/userinfo', 'FAIL', 'Invalid access token')
|
||||||
|
return jsonify({"error": "invalid_token"}), 401
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if not all([BASE_URL, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, PEERTUBE_CALLBACK_URL]):
|
||||||
|
print("FATAL ERROR: One or more required environment variables are not set.")
|
||||||
|
print("Please check your .env file.")
|
||||||
|
else:
|
||||||
|
app.run(host='0.0.0.0', port=5000)
|
||||||
21
docker-compose.yaml
Normal file
21
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# docker-compose.yml
|
||||||
|
# This file defines and runs our custom OIDC bridge application.
|
||||||
|
# The PeerTube instance will connect to this service.
|
||||||
|
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# The OIDC Bridge Service
|
||||||
|
# Translates Discord OAuth2 into an OIDC-compliant flow for PeerTube.
|
||||||
|
oidc-bridge:
|
||||||
|
# We build the image from the local Dockerfile.
|
||||||
|
build: .
|
||||||
|
# The container will restart automatically if it fails.
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# Expose the service on port 5000.
|
||||||
|
# PeerTube will connect to this port.
|
||||||
|
- "5000:5000"
|
||||||
|
# Mount the .env file to be used by the Python application.
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
Flask==2.1.2
|
||||||
|
Werkzeug==2.0.3
|
||||||
|
requests==2.27.1
|
||||||
|
cryptography==37.0.2
|
||||||
|
python-dotenv==0.20.0
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
99
templates/status.html
Normal file
99
templates/status.html
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OIDC Bridge Status</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Inter', sans-serif; }
|
||||||
|
.status-ok { color: #4ade80; } /* green-400 */
|
||||||
|
.status-fail { color: #f87171; } /* red-400 */
|
||||||
|
.status-info { color: #60a5fa; } /* blue-400 */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 text-white flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div class="bg-gray-800 p-8 rounded-lg shadow-2xl w-full max-w-4xl">
|
||||||
|
<h1 class="text-2xl font-bold text-cyan-400 mb-6">OIDC Bridge Service Status</h1>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Environment Variable Checks -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-300 border-b border-gray-600 pb-2 mb-3">Configuration Checks</h2>
|
||||||
|
<ul class="space-y-2 text-sm">
|
||||||
|
{% for key, value in env_checks.items() %}
|
||||||
|
<li class="flex justify-between items-center bg-gray-700 p-3 rounded-md">
|
||||||
|
<span class="font-mono text-gray-400">{{ key }}</span>
|
||||||
|
{% if value == True or value == 'Set' %}
|
||||||
|
<span class="font-bold status-ok">SET</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="font-bold status-fail">NOT SET</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connectivity Checks -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-300 border-b border-gray-600 pb-2 mb-3">Connectivity Checks</h2>
|
||||||
|
<ul class="space-y-2 text-sm">
|
||||||
|
<li class="flex justify-between items-center bg-gray-700 p-3 rounded-md">
|
||||||
|
<span class="font-mono text-gray-400">Discord API</span>
|
||||||
|
{% if discord_api_status == 'OK' %}
|
||||||
|
<span class="font-bold status-ok">OK</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="font-bold status-fail">{{ discord_api_status }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column for Recent Requests -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-300 border-b border-gray-600 pb-2 mb-3">Recent Incoming Requests</h2>
|
||||||
|
<div class="bg-gray-700 rounded-md p-1 max-h-80 overflow-y-auto">
|
||||||
|
<table class="w-full text-sm text-left">
|
||||||
|
<thead class="text-xs text-gray-400 uppercase bg-gray-700 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-4 py-2">Timestamp</th>
|
||||||
|
<th scope="col" class="px-4 py-2">Endpoint</th>
|
||||||
|
<th scope="col" class="px-4 py-2">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if requests %}
|
||||||
|
{% for req in requests %}
|
||||||
|
<tr class="border-b border-gray-600">
|
||||||
|
<td class="px-4 py-2 text-gray-300 font-mono">{{ req.timestamp }}</td>
|
||||||
|
<td class="px-4 py-2 font-mono">{{ req.endpoint }}</td>
|
||||||
|
<td class="px-4 py-2 font-bold">
|
||||||
|
{% if req.status == 'OK' %}
|
||||||
|
<span class="status-ok">{{ req.status }}</span>
|
||||||
|
{% elif req.status == 'FAIL' %}
|
||||||
|
<span class="status-fail" title="{{ req.error }}">{{ req.status }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-info">{{ req.status }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center py-4 text-gray-500">No requests from PeerTube received yet.</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-gray-500 mt-8 text-center">This page can be used to diagnose issues with the OIDC Bridge service.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue