Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| beb4f96d81 |
22 changed files with 1095 additions and 1965 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -2,10 +2,4 @@
|
||||||
*.swp
|
*.swp
|
||||||
env/
|
env/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
data/
|
|
||||||
|
|
||||||
cookies.txt
|
cookies.txt
|
||||||
jwt_secret.txt
|
|
||||||
pyralogs_secret.txt
|
|
||||||
|
|
||||||
|
|
|
||||||
124
.gitlab-ci.yml
124
.gitlab-ci.yml
|
|
@ -1,124 +0,0 @@
|
||||||
stages:
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
variables:
|
|
||||||
APP_NAME: "logs.unturf.com"
|
|
||||||
APP_DIR: "/opt/${APP_NAME}"
|
|
||||||
VENV_DIR: "${APP_DIR}/venv"
|
|
||||||
CADDYFILE_DIR: "/etc/caddy"
|
|
||||||
CADDY_SNIPPETS_DIR: "/etc/caddy/caddy_snippets"
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
stage: deploy
|
|
||||||
tags:
|
|
||||||
- master.unturf.com
|
|
||||||
only:
|
|
||||||
- main # Adjust if you want to deploy from other branches
|
|
||||||
script:
|
|
||||||
# Step 1: Define variables with substitutions
|
|
||||||
- |
|
|
||||||
# Variables with substitutions must be defined in the script section
|
|
||||||
SERVICE_NAME="uwsgi-${APP_NAME//./-}.service" # Replace dots with dashes
|
|
||||||
UWSGI_SOCKET="${APP_DIR}/uwsgi.sock" # Place socket in app directory
|
|
||||||
|
|
||||||
# Step 2: Synchronize the code to the application directory
|
|
||||||
- |
|
|
||||||
if [ ! -d "${APP_DIR}" ]; then
|
|
||||||
mkdir -p "${APP_DIR}"
|
|
||||||
fi
|
|
||||||
rsync -a --exclude=data/ --exclude=venv/ --delete "${CI_PROJECT_DIR}/" "${APP_DIR}/"
|
|
||||||
|
|
||||||
# Step 3: Set up Python virtual environment and install dependencies
|
|
||||||
- |
|
|
||||||
if [ ! -d "${VENV_DIR}" ]; then
|
|
||||||
python3 -m venv "${VENV_DIR}"
|
|
||||||
fi
|
|
||||||
source "${VENV_DIR}/bin/activate"
|
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -r "${APP_DIR}/requirements.txt"
|
|
||||||
|
|
||||||
# Step 4: Install uWSGI in virtual environment
|
|
||||||
- |
|
|
||||||
source "${VENV_DIR}/bin/activate"
|
|
||||||
pip install uwsgi
|
|
||||||
|
|
||||||
# Step 5: Create or update the systemd user service file for uWSGI
|
|
||||||
- |
|
|
||||||
mkdir -p ~/.config/systemd/user/
|
|
||||||
|
|
||||||
# Determine the correct module and callable for your application
|
|
||||||
# Adjust 'app:app' based on your application's entry point
|
|
||||||
# For example, if your main application file is 'app.py' and the Flask app is named 'app', use 'app:app'
|
|
||||||
|
|
||||||
# If your application entry point is different, adjust accordingly
|
|
||||||
# For example, if your main file is 'wsgi.py' and the callable is 'application', use 'wsgi:application'
|
|
||||||
|
|
||||||
# Set the MODULE variable based on your application's entry point
|
|
||||||
MODULE="app:uwsgi_app" # Adjust this based on your application
|
|
||||||
|
|
||||||
cat > ~/.config/systemd/user/${SERVICE_NAME} <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=uWSGI instance to serve ${APP_NAME}
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
WorkingDirectory=${APP_DIR}
|
|
||||||
Environment="PATH=${VENV_DIR}/bin"
|
|
||||||
ExecStart=${VENV_DIR}/bin/uwsgi \\
|
|
||||||
--master \\
|
|
||||||
--enable-threads \\
|
|
||||||
--processes 4 \\
|
|
||||||
--http-socket ${UWSGI_SOCKET} \\
|
|
||||||
--chmod-socket=660 \\
|
|
||||||
--chown-socket=gitlab-runner:caddy \\
|
|
||||||
--vacuum \\
|
|
||||||
--die-on-term \\
|
|
||||||
--chdir ${APP_DIR} \\
|
|
||||||
--logto2 ${APP_DIR}/uwsgi.log \\
|
|
||||||
--module ${MODULE}
|
|
||||||
|
|
||||||
Restart=always
|
|
||||||
KillSignal=SIGQUIT
|
|
||||||
Type=notify
|
|
||||||
NotifyAccess=all
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Step 6: Reload systemd user daemon and restart the uWSGI service
|
|
||||||
- systemctl --user daemon-reload
|
|
||||||
- systemctl --user restart ${SERVICE_NAME}
|
|
||||||
- systemctl --user enable ${SERVICE_NAME}
|
|
||||||
|
|
||||||
# Step 7: Handle Caddy configurations
|
|
||||||
- |
|
|
||||||
# Create Caddy snippets directory if it doesn't exist
|
|
||||||
mkdir -p "${CADDY_SNIPPETS_DIR}"
|
|
||||||
|
|
||||||
# Write the application's Caddy configuration to its own snippet file
|
|
||||||
cat > "${CADDY_SNIPPETS_DIR}/${APP_NAME}.caddy" <<EOF
|
|
||||||
${APP_NAME} {
|
|
||||||
encode gzip
|
|
||||||
tls {
|
|
||||||
on_demand
|
|
||||||
}
|
|
||||||
handle {
|
|
||||||
reverse_proxy unix//${UWSGI_SOCKET}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Concatenate all snippets into the main Caddyfile
|
|
||||||
echo '# Main Caddyfile - concatenated snippets' > "${CADDYFILE_DIR}/Caddyfile"
|
|
||||||
cat "${CADDY_SNIPPETS_DIR}"/*.caddy >> "${CADDYFILE_DIR}/Caddyfile"
|
|
||||||
|
|
||||||
# Validate the Caddyfile
|
|
||||||
caddy validate --config "${CADDYFILE_DIR}/Caddyfile"
|
|
||||||
|
|
||||||
# Step 8: Reload Caddy service, gitlab-runner is in sudoers.
|
|
||||||
# gitlab-runner ALL=(ALL) NOPASSWD: /bin/systemctl * caddy.service
|
|
||||||
- sudo /bin/systemctl restart caddy.service
|
|
||||||
|
|
||||||
# Deployment completed
|
|
||||||
- echo "Deployment completed successfully."
|
|
||||||
200
README.rst
200
README.rst
|
|
@ -1,19 +1,18 @@
|
||||||
================================
|
===========================================
|
||||||
PyraLogs - A Centralized Logging Hub
|
pyrafiles - A Sophisticated Pyramid Project
|
||||||
================================
|
===========================================
|
||||||
|
|
||||||
**PyraLogs** is a **public domain** application built on the `Pyramid <https://trypyramid.com>`_ framework. It offers a flexible, agent-friendly solution for centralized logging, allowing environments and agents to log events to a central hub. PyraLogs supports a multi-database design where each namespace maintains its own SQLite database for logs. The main database (``main.db``) stores system- and user-level data, while each namespace’s personal DB file handles their specific logs.
|
``pyrafiles`` is a **public domain** application built on the `Pyramid <https://trypyramid.com>`_ framework. It demonstrates a flexible, multi-database design where each verified user maintains their own SQLite database for media uploads (images, audio, video). The main database (``main.db``) stores system- and user-level data, while each user’s personal DB file handles their specific uploads.
|
||||||
|
|
||||||
Key Features
|
Key Features
|
||||||
============
|
============
|
||||||
|
|
||||||
- **Passwordless Login** using email verification codes.
|
- **Passwordless Login** using email verification codes.
|
||||||
- **Namespace-Based Logging** with per-namespace SQLite databases.
|
- **Guest Sessions** for unverified visitors.
|
||||||
- **JWT Authentication for Agents**, enabling secure log submissions.
|
- **Per-user SQLite** databases for media uploads (limit ~30MB each).
|
||||||
- **Token Versioning** for per-agent token revocation without affecting others.
|
- **Public/Private** media visibility controls.
|
||||||
- **Public/Private** namespace visibility controls.
|
- **Admin Tools** (import user records, manage site settings).
|
||||||
- **Admin Tools** for managing namespaces, agents, and user access.
|
- **Agent-friendly** architecture: easily scriptable endpoints for login, verification, media uploads, etc.
|
||||||
- **Agent-Friendly** architecture: easily scriptable endpoints for logging and management.
|
|
||||||
- **Configurable** via environment variables (including secret keys).
|
- **Configurable** via environment variables (including secret keys).
|
||||||
|
|
||||||
Git Repository
|
Git Repository
|
||||||
|
|
@ -21,36 +20,32 @@ Git Repository
|
||||||
|
|
||||||
The project is maintained at:
|
The project is maintained at:
|
||||||
|
|
||||||
- `PyraLogs Git Repo <https://git.unturf.com/engineering/unturf/logs.unturf.com>`_
|
- `pyrafiles Git Repo <https://git.unturf.com/engineering/unturf/upload.unturf.com>`_
|
||||||
|
|
||||||
Since this is public domain, you can adapt and redistribute it freely.
|
Since this is public domain, you can adapt and redistribute it freely.
|
||||||
|
|
||||||
|
|
||||||
Configuration via Environment
|
Configuration via Environment
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
PyraLogs fetches settings from environment variables with sensible defaults:
|
`pyrafiles` fetches settings from environment variables with sensible defaults:
|
||||||
|
|
||||||
- ``PYRALOGS_SECRET``
|
- ``PYRAFILES_SECRET``
|
||||||
The secret key for cookie session signing.
|
The secret key for session signing.
|
||||||
If missing, PyraLogs automatically generates a **random 64-character** string at runtime.
|
If missing, ``pyrafiles`` automatically generates a **random 64-character** string at runtime & log all users out.
|
||||||
Remove this file or change this value to log our all cookie sessions for all namespaces.
|
|
||||||
|
|
||||||
- ``PYRALOGS_JWT_SECRET``
|
- ``PYRAFILES_DB_URL``
|
||||||
The secret key for signing JSON Web Tokens (JWTs) for agents without a mailbox.
|
|
||||||
If missing, PyraLogs automatically generates a **random 64-character** string at runtime.
|
|
||||||
Remove this file or change this value to log out all JWT agents for all namespaces.
|
|
||||||
|
|
||||||
- ``PYRALOGS_DB_URL``
|
|
||||||
Connection string for the main database. Default: ``sqlite:///main.db``.
|
Connection string for the main database. Default: ``sqlite:///main.db``.
|
||||||
|
|
||||||
- ``PYRALOGS_HOST`` and ``PYRALOGS_PORT``
|
- ``PYRAFILES_HOST`` and ``PYRAFILES_PORT``
|
||||||
The host and port to serve on. Defaults: ``0.0.0.0`` (host), ``6544`` (port).
|
The host and port to serve on. Defaults: ``0.0.0.0`` (host), ``6544`` (port).
|
||||||
|
|
||||||
- ``PYRALOGS_SMTP_HOST`` and ``PYRALOGS_SMTP_PORT``
|
- ``PYRAFILES_SMTP_HOST`` and ``PYRAFILES_SMTP_PORT``
|
||||||
SMTP server details. Defaults: ``localhost:25``.
|
SMTP server details. Defaults: ``localhost:25``.
|
||||||
|
|
||||||
- Any other environment variables you wish to incorporate can be accessed in the code.
|
- Any other environment variables you wish to incorporate can be accessed in the code.
|
||||||
|
|
||||||
|
|
||||||
Local Setup
|
Local Setup
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
|
@ -58,8 +53,8 @@ Local Setup
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
git clone https://git.unturf.com/engineering/unturf/logs.unturf.com.git
|
git clone https://git.unturf.com/engineering/unturf/upload.unturf.com.git
|
||||||
cd logs.unturf.com
|
cd upload.unturf.com
|
||||||
|
|
||||||
2. **Create a Virtual Environment**
|
2. **Create a Virtual Environment**
|
||||||
|
|
||||||
|
|
@ -76,78 +71,126 @@ Local Setup
|
||||||
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
4. **Initialize the Database**
|
4. **Run**
|
||||||
|
|
||||||
Create the main database before running the application.
|
Create the database if this is the first time running the applicaiton.
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
python initialize_db.py
|
python initialize_db.py
|
||||||
|
|
||||||
5. **Run**
|
Otherwise:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
# Optionally set PYRALOGS_SECRET if you want a custom secret.
|
# Optionally set PYRAFILES_SECRET if you want a custom secret.
|
||||||
export PYRALOGS_SECRET="YOUR_OWN_LONG_RANDOM_STRING"
|
export PYRAFILES_SECRET="YOUR_OWN_LONG_RANDOM_STRING"
|
||||||
python app.py
|
python main.py
|
||||||
|
|
||||||
If ``PYRALOGS_SECRET`` is **not** set, the app automatically generates a 64-character secret at runtime.
|
If ``PYRAFILES_SECRET`` is **not** set, the app automatically generates a 64-char secret at runtime & log all users out.
|
||||||
|
|
||||||
6. **Access**
|
5. **Access**
|
||||||
|
|
||||||
Point your browser to `http://localhost:6544` or `http://<HOST>:<PORT>` according to your environment variables.
|
Point your browser to `http://localhost:6544` or `http://<HOST>:<PORT>` according to your environment variables.
|
||||||
|
|
||||||
|
|
||||||
Example: Agent Workflow Script
|
Example: Agent Workflow Script
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
Below is a sample Bash script showing how an **agent** might:
|
Below is a sample Bash script showing how an **agent** might:
|
||||||
|
|
||||||
1. Obtain a JWT for logging.
|
1. Start the session (to get a cookie).
|
||||||
2. Send logs to the PyraLogs server using the JWT.
|
2. Log in with an email.
|
||||||
|
3. Prompt the user for a verification code.
|
||||||
|
4. Complete the verification.
|
||||||
|
5. Upload a file.
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./test_agent.sh "Your log message"
|
# MAX_COOKIE_AGE=1800 ./agent_upload.sh /path/to/somefile.jpg
|
||||||
#
|
#
|
||||||
# Description:
|
# Description:
|
||||||
# Send a log message to the PyraLogs server using a JWT.
|
# Upload a file to the pyrafiles server as an agent, making it public.
|
||||||
|
# - If cookies.txt is newer than MAX_COOKIE_AGE seconds (default 31536000, ~1 year),
|
||||||
|
# we skip re-login.
|
||||||
|
# - Otherwise, we prompt for OTP again.
|
||||||
#
|
#
|
||||||
# Notes:
|
# Notes:
|
||||||
# - Replace <YOUR_NAMESPACE_JWT> with your actual JWT token.
|
# 1. If not set, MAX_COOKIE_AGE defaults to 31536000 (one year).
|
||||||
# - Replace <NAMESPACE_SHORT_ID> with your namespace's short ID.
|
# 2. We assume the server runs on http://localhost:6544. Adjust BASE_URL if needed.
|
||||||
# - Adjust BASE_URL if the server is running elsewhere.
|
|
||||||
|
|
||||||
BASE_URL="${BASE_URL:-http://localhost:6544}"
|
BASE_URL="${BASE_URL:-http://localhost:6544}"
|
||||||
JWT_TOKEN="<YOUR_NAMESPACE_JWT>"
|
EMAIL="${EMAIL:-agent@example.com}"
|
||||||
LOG_MESSAGE="$1"
|
MEDIA_FILE="$1"
|
||||||
|
# Default to 1 year in seconds if not provided:
|
||||||
|
MAX_COOKIE_AGE="${MAX_COOKIE_AGE:-31536000}"
|
||||||
|
|
||||||
if [[ -z "$LOG_MESSAGE" ]]; then
|
if [[ -z "$MEDIA_FILE" ]]; then
|
||||||
echo "Usage: $0 \"Your log message\""
|
echo "Usage: MAX_COOKIE_AGE=<seconds> $0 /path/to/mediafile"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Sending log message..."
|
# Check if cookies.txt is 'fresh enough':
|
||||||
RESPONSE=$(curl -s -X POST "$BASE_URL/webhook" \
|
COOKIE_FRESH=0
|
||||||
-H "Content-Type: application/json" \
|
if [[ -f cookies.txt ]]; then
|
||||||
-H "Authorization: Bearer $JWT_TOKEN" \
|
# 'stat -c %Y' returns the file's mod time on Linux; 'stat -f %m' on macOS/BSD
|
||||||
-d "{
|
MOD_TIME=$(stat -c %Y cookies.txt 2>/dev/null || stat -f %m cookies.txt 2>/dev/null)
|
||||||
\"message\": \"$LOG_MESSAGE\",
|
NOW=$(date +%s)
|
||||||
\"level\": \"INFO\",
|
AGE=$((NOW - MOD_TIME))
|
||||||
\"metadata\": {\"w\": 1}
|
|
||||||
}")
|
if (( AGE < MAX_COOKIE_AGE )); then
|
||||||
|
COOKIE_FRESH=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$COOKIE_FRESH" -eq 1 ]]; then
|
||||||
|
echo "Reusing existing cookies (file age < $MAX_COOKIE_AGE seconds)."
|
||||||
|
echo "Skipping OTP prompt."
|
||||||
|
else
|
||||||
|
echo "Starting new session (cookie missing or stale)..."
|
||||||
|
curl -s -c cookies.txt -b cookies.txt "$BASE_URL/" >/dev/null
|
||||||
|
|
||||||
|
echo "Logging in with email: $EMAIL"
|
||||||
|
curl -s -c cookies.txt -b cookies.txt \
|
||||||
|
-X POST \
|
||||||
|
-F "email=$EMAIL" \
|
||||||
|
"$BASE_URL/auth/login"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Check your console or email for the 6-digit verification code."
|
||||||
|
read -p "Enter the 6-digit code: " VERIFICATION_CODE
|
||||||
|
|
||||||
|
echo "Verifying code..."
|
||||||
|
curl -s -c cookies.txt -b cookies.txt \
|
||||||
|
-X POST \
|
||||||
|
-F "code=$VERIFICATION_CODE" \
|
||||||
|
"$BASE_URL/auth/verify"
|
||||||
|
|
||||||
|
echo "Cookies refreshed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Uploading media: $MEDIA_FILE (public)"
|
||||||
|
curl -s -c cookies.txt -b cookies.txt \
|
||||||
|
-X POST \
|
||||||
|
-F "media_file=@$MEDIA_FILE" \
|
||||||
|
-F "is_public=on" \
|
||||||
|
"$BASE_URL/media/upload"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Upload complete. File is public."
|
||||||
|
|
||||||
|
|
||||||
echo "Server Response: $RESPONSE"
|
|
||||||
|
|
||||||
Dockerfile with Caddy + uWSGI
|
Dockerfile with Caddy + uWSGI
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
Below is an example Dockerfile that:
|
Below is an example Dockerfile that:
|
||||||
|
|
||||||
- Uses **uWSGI** to run PyraLogs.
|
- Uses **uWSGI** to run ``pyrafiles``.
|
||||||
- Uses **Caddy** as a reverse proxy (and optionally HTTPS if configured).
|
- Uses **Caddy** as a reverse proxy (and optionally HTTPS if configured).
|
||||||
- Defines a **volume** for databases (so they are stored on the host).
|
- Defines a **volume** for databases (so they are stored on the host).
|
||||||
|
|
||||||
|
|
@ -184,7 +227,7 @@ Below is an example Dockerfile that:
|
||||||
# Copy uWSGI config if desired
|
# Copy uWSGI config if desired
|
||||||
# For example (assume you created uwsgi.ini in your repo):
|
# For example (assume you created uwsgi.ini in your repo):
|
||||||
# [uwsgi]
|
# [uwsgi]
|
||||||
# module = app:app
|
# module = main:app
|
||||||
# master = true
|
# master = true
|
||||||
# processes = 4
|
# processes = 4
|
||||||
# socket = 127.0.0.1:8080
|
# socket = 127.0.0.1:8080
|
||||||
|
|
@ -193,19 +236,22 @@ Below is an example Dockerfile that:
|
||||||
COPY uwsgi.ini /app/uwsgi.ini
|
COPY uwsgi.ini /app/uwsgi.ini
|
||||||
|
|
||||||
# Environment variables (optional overrides).
|
# Environment variables (optional overrides).
|
||||||
ENV PYRALOGS_DB_URL="sqlite:///data/main.db"
|
# If PYRAFILES_SECRET is empty, the app itself generates a random 64-char secret & logs all users out.
|
||||||
ENV PYRALOGS_HOST="0.0.0.0"
|
ENV PYRAFILES_SECRET=""
|
||||||
ENV PYRALOGS_PORT="6544"
|
ENV PYRAFILES_DB_URL="sqlite:///data/main.db"
|
||||||
|
ENV PYRAFILES_HOST="0.0.0.0"
|
||||||
|
ENV PYRAFILES_PORT="6544"
|
||||||
|
|
||||||
# Expose HTTP and HTTPS
|
# Expose HTTP and HTTPS
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|
||||||
# Define volume so host can persist databases outside the container
|
# Define volume so host can persist user DBs outside the container
|
||||||
# We'll store database files in /data
|
# We'll store DB files in /data
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
# Command starts uWSGI and then starts Caddy
|
# Command starts uWSGI and then starts Caddy
|
||||||
|
# The app itself checks for PYRAFILES_SECRET and auto-generates one if missing.
|
||||||
CMD ["/bin/sh", "-c", "\
|
CMD ["/bin/sh", "-c", "\
|
||||||
uwsgi --ini /app/uwsgi.ini & \
|
uwsgi --ini /app/uwsgi.ini & \
|
||||||
caddy run --config /etc/caddy/Caddyfile \
|
caddy run --config /etc/caddy/Caddyfile \
|
||||||
|
|
@ -213,8 +259,8 @@ Below is an example Dockerfile that:
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
- We use ``/data`` as the volume. By default, the environment variable ``PYRALOGS_DB_URL`` is set to ``sqlite:///data/main.db``, so the main DB (and any namespace DB files) go inside ``/data``.
|
- We use ``/data`` as the volume. By default, the environment variable ``PYRAFILES_DB_URL`` is set to ``sqlite:///data/main.db``, so the main DB (and any user DB files) go inside ``/data``.
|
||||||
- For namespace DB files, your app can also interpret an environment variable (like ``PYRALOGS_DB_BASEPATH=/data``) if you want to make that path configurable in the code.
|
- For user DB files, your app can also interpret an environment variable (like ``PYRAFILES_DB_BASEPATH=/data``) if you want to make that path configurable in the code.
|
||||||
- Make sure to **mount** a volume at ``/data`` when you run the container, e.g.:
|
- Make sure to **mount** a volume at ``/data`` when you run the container, e.g.:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
@ -222,34 +268,36 @@ Below is an example Dockerfile that:
|
||||||
docker run -d \
|
docker run -d \
|
||||||
-p 80:80 -p 443:443 \
|
-p 80:80 -p 443:443 \
|
||||||
-v /my/local/dbfolder:/data \
|
-v /my/local/dbfolder:/data \
|
||||||
--name pyralogs \
|
--name pyrafiles \
|
||||||
pyralogs-image:latest
|
pyrafiles-image:latest
|
||||||
|
|
||||||
|
- With that, any user database is stored in ``/my/local/dbfolder`` on the host.
|
||||||
|
|
||||||
- With that, any namespace database is stored in ``/my/local/dbfolder`` on the host.
|
|
||||||
|
|
||||||
Tips
|
Tips
|
||||||
====
|
====
|
||||||
|
|
||||||
- For **production**, you likely want to set up a real SMTP server or third-party service (e.g., Mailgun, Improvmx) and configure it via environment variables (``PYRALOGS_SMTP_HOST``, etc.).
|
- For **production**, you likely want to set up a real SMTP server or third-party service (e.g. Mailgun, ImprovMV) and configure it via environment variables (``PYRAFILES_SMTP_HOST``, etc.).
|
||||||
- Ensure you mount a volume for persistent SQLite files if you want to avoid data loss when containers are removed or replaced.
|
- Ensure you mount a volume for persistent SQLite files if you want to avoid data loss when containers are removed or replaced.
|
||||||
- Because the project is **public domain**, you can adapt it without restriction, removing features, adding custom logic, etc.
|
- Because the project is **public domain**, you can adapt it without restriction, removing features, adding custom logic, etc.
|
||||||
- **Security Reminder**: Always keep your ``PYRALOGS_SECRET`` and ``PYRALOGS_JWT_SECRET`` secure. Do not expose them in your code repositories or logs.
|
|
||||||
|
|
||||||
License & Public Domain
|
|
||||||
|
License and Public Domain
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
This project is in the public domain. You are free to use, adapt, and redistribute it without attribution or additional licensing.
|
This project is in the public domain. You are free to use, adapt, and redistribute it without attribution or additional licensing.
|
||||||
|
|
||||||
If you find PyraLogs helpful, feel free to contribute back or share your enhancements!
|
If you find `pyrafiles` helpful, feel free to contribute back or share your enhancements!
|
||||||
|
|
||||||
|
|
||||||
Support and Contact
|
Support and Contact
|
||||||
===================
|
===================
|
||||||
|
|
||||||
If you register a gitlab account, let me know, and I will grant you developer access to contribute.
|
- Issues: Please open tickets at the `git.unturf.com project page <https://git.unturf.com/engineering/unturf/upload.unturf.com>`_.
|
||||||
|
|
||||||
- **Issues**: Please open tickets at the `PyraLogs project page <https://git.unturf.com/engineering/unturf/logs.unturf.com>`_.
|
|
||||||
- For general inquiries, you can reach out to the maintainers directly.
|
- For general inquiries, you can reach out to the maintainers directly.
|
||||||
|
|
||||||
We hope PyraLogs helps you set up a centralized logging solution quickly and efficiently!
|
We hope `pyrafiles` helps you get up and running quickly with a flexible media-sharing and multi-DB infrastructure!
|
||||||
|
|
||||||
Enjoy and happy logging!
|
If you register an account, let me know and I will bless you as an developer to contribute.
|
||||||
|
|
||||||
|
Enjoy and happy building!
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""
|
"""
|
||||||
Initialize or upgrade the main database for PyraLogs.
|
Initialize or upgrade the main database for pyrafiles.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python initialize_db.py
|
python initialize_db.py
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
PYRAFILES_DB_URL (optional):
|
||||||
|
The SQLAlchemy database URL (e.g., "sqlite:///main.db").
|
||||||
|
Defaults to "sqlite:///main.db" if not set.
|
||||||
|
|
||||||
Description:
|
Description:
|
||||||
Creates or updates all tables referenced by `Base.metadata`.
|
Creates or updates all tables referenced by `Base.metadata`.
|
||||||
The database file will be created (if it does not exist) in the
|
If the database file/tables do not exist, they will be created.
|
||||||
DATA_DIR as specified in the app file. No custom paths are allowed.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
@ -20,8 +24,11 @@ from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
# Import Base and DATA_DIR from your app.py (Adjust if needed)
|
# Adjust these imports to your actual project structure:
|
||||||
from app import Base, DATA_DIR # Make sure app.py defines Base and DATA_DIR
|
# For example, if your models and Base are defined in "models.py", do:
|
||||||
|
# from pyrafiles import Base
|
||||||
|
# If you keep Base in a separate module, adjust accordingly.
|
||||||
|
from app import Base # or from your_project.models import Base
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
script = os.path.basename(sys.argv[0])
|
script = os.path.basename(sys.argv[0])
|
||||||
|
|
@ -32,25 +39,19 @@ def usage():
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
if len(sys.argv) > 2:
|
||||||
# We do not allow any command-line arguments.
|
# We only expect optional arguments. If needed, parse them here.
|
||||||
if len(sys.argv) > 1:
|
|
||||||
usage()
|
usage()
|
||||||
|
|
||||||
# Ensure DATA_DIR exists
|
# Read environment variable for DB URL
|
||||||
if not os.path.exists(DATA_DIR):
|
db_url = os.environ.get("PYRAFILES_DB_URL", "sqlite:///main.db")
|
||||||
os.makedirs(DATA_DIR)
|
|
||||||
logging.info(f"Created data directory at {DATA_DIR}")
|
|
||||||
|
|
||||||
# Always use the default path from app.py for the database
|
|
||||||
db_url = f"sqlite:///{os.path.join(DATA_DIR, 'main.db')}"
|
|
||||||
logging.info(f"Using DB URL: {db_url}")
|
logging.info(f"Using DB URL: {db_url}")
|
||||||
|
|
||||||
# Set up the engine
|
# Set up engine
|
||||||
engine = create_engine(
|
engine = create_engine(
|
||||||
db_url,
|
db_url,
|
||||||
connect_args={"check_same_thread": False},
|
connect_args={"check_same_thread": False} if "sqlite" in db_url else {},
|
||||||
poolclass=StaticPool
|
poolclass=StaticPool if "sqlite" in db_url else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
SessionFactory = sessionmaker(bind=engine)
|
SessionFactory = sessionmaker(bind=engine)
|
||||||
|
|
@ -64,3 +65,4 @@ def main():
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
|
||||||
580
openapi.yaml
580
openapi.yaml
|
|
@ -1,47 +1,11 @@
|
||||||
openapi: 3.0.3
|
openapi: 3.0.0
|
||||||
info:
|
info:
|
||||||
title: PyraLogs API
|
title: PyraFiles API
|
||||||
version: 1.0.0
|
version: '1.0'
|
||||||
description: |
|
|
||||||
API specification for the PyraLogs application.
|
|
||||||
PyraLogs allows users to register, authenticate, manage namespaces,
|
|
||||||
generate JWT tokens for agents, and submit logs via a webhook.
|
|
||||||
servers:
|
|
||||||
- url: http://127.0.0.1:{port}
|
|
||||||
description: Local development server
|
|
||||||
variables:
|
|
||||||
port:
|
|
||||||
default: '6544'
|
|
||||||
- url: https://logs.unturf.com
|
|
||||||
description: Production server
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/:
|
|
||||||
get:
|
|
||||||
summary: Home Page
|
|
||||||
description: Displays the home page with public namespaces and user namespaces.
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Successful response
|
|
||||||
content:
|
|
||||||
text/html:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
|
|
||||||
/auth/login:
|
/auth/login:
|
||||||
get:
|
|
||||||
summary: Display Login Page
|
|
||||||
description: Renders the login page where users can enter their email.
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Login page rendered
|
|
||||||
content:
|
|
||||||
text/html:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
post:
|
post:
|
||||||
summary: Process Login
|
summary: User login
|
||||||
description: Sends a verification code to the user's email.
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
|
|
@ -51,36 +15,12 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
format: email
|
|
||||||
required:
|
|
||||||
- email
|
|
||||||
responses:
|
responses:
|
||||||
'302':
|
'302':
|
||||||
description: Redirects to the verification page
|
description: Redirect to verification page
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the verification page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., email missing)
|
|
||||||
'500':
|
|
||||||
description: Internal Server Error
|
|
||||||
|
|
||||||
/auth/verify:
|
/auth/verify:
|
||||||
get:
|
|
||||||
summary: Display Verification Page
|
|
||||||
description: Renders the verification page where users can enter their code.
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Verification page rendered
|
|
||||||
content:
|
|
||||||
text/html:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
post:
|
post:
|
||||||
summary: Verify User
|
summary: Verify user code
|
||||||
description: Verifies the user's code and logs them in.
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
|
|
@ -90,514 +30,24 @@ paths:
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- code
|
|
||||||
responses:
|
responses:
|
||||||
'302':
|
'302':
|
||||||
description: Redirects to the home page upon successful verification
|
description: Redirect to home page
|
||||||
headers:
|
/media/upload:
|
||||||
Location:
|
|
||||||
description: URL of the home page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., invalid code)
|
|
||||||
'500':
|
|
||||||
description: Internal Server Error
|
|
||||||
|
|
||||||
/auth/logout:
|
|
||||||
post:
|
post:
|
||||||
summary: Logout User
|
summary: Upload media file
|
||||||
description: Logs out the current user.
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/x-www-form-urlencoded:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
csrf_token:
|
media_file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
title:
|
||||||
type: string
|
type: string
|
||||||
responses:
|
responses:
|
||||||
'302':
|
'302':
|
||||||
description: Redirects to the home page
|
description: Redirect to media details page
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the home page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'403':
|
|
||||||
description: Unauthorized (user not logged in)
|
|
||||||
|
|
||||||
/auth/profile:
|
|
||||||
get:
|
|
||||||
summary: Display User Profile
|
|
||||||
description: Shows the user's profile, including owned namespaces.
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Profile page rendered
|
|
||||||
content:
|
|
||||||
text/html:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'403':
|
|
||||||
description: Unauthorized (user not logged in)
|
|
||||||
post:
|
|
||||||
summary: Update User Profile
|
|
||||||
description: Updates the user's profile settings.
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
enable_gravatar:
|
|
||||||
type: string
|
|
||||||
enum: ['on']
|
|
||||||
new_username:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- new_username
|
|
||||||
encoding:
|
|
||||||
enable_gravatar:
|
|
||||||
contentType: text/plain
|
|
||||||
new_username:
|
|
||||||
contentType: text/plain
|
|
||||||
responses:
|
|
||||||
'302':
|
|
||||||
description: Redirects to the profile page
|
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the profile page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., username already in use)
|
|
||||||
'403':
|
|
||||||
description: Unauthorized (user not logged in or guest mode)
|
|
||||||
|
|
||||||
/namespace/create:
|
|
||||||
get:
|
|
||||||
summary: Display Namespace Creation Page
|
|
||||||
description: Renders the form to create a new namespace.
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Namespace creation page rendered
|
|
||||||
content:
|
|
||||||
text/html:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'403':
|
|
||||||
description: Unauthorized (user not logged in)
|
|
||||||
post:
|
|
||||||
summary: Create Namespace
|
|
||||||
description: Processes the namespace creation form.
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
is_public:
|
|
||||||
type: string
|
|
||||||
enum: ['on']
|
|
||||||
encoding:
|
|
||||||
name:
|
|
||||||
contentType: text/plain
|
|
||||||
is_public:
|
|
||||||
contentType: text/plain
|
|
||||||
responses:
|
|
||||||
'302':
|
|
||||||
description: Redirects to the namespace management page
|
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the namespace management page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., namespace name already exists)
|
|
||||||
'403':
|
|
||||||
description: Unauthorized (user not logged in)
|
|
||||||
|
|
||||||
/namespace/{namespace_short_id}/manage:
|
|
||||||
get:
|
|
||||||
summary: Manage Namespace
|
|
||||||
description: Displays the namespace management page (owners only).
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: namespace_short_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The short ID of the namespace
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Namespace management page rendered
|
|
||||||
content:
|
|
||||||
text/html:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'403':
|
|
||||||
description: Forbidden (not owner or not logged in)
|
|
||||||
'404':
|
|
||||||
description: Namespace not found
|
|
||||||
|
|
||||||
/namespace/{namespace_short_id}/update:
|
|
||||||
post:
|
|
||||||
summary: Update Namespace
|
|
||||||
description: Updates namespace properties (owners only).
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: namespace_short_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The short ID of the namespace
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
is_public:
|
|
||||||
type: string
|
|
||||||
enum: ['on']
|
|
||||||
description: 'Checkbox value. Present if checked.'
|
|
||||||
encoding:
|
|
||||||
is_public:
|
|
||||||
contentType: text/plain
|
|
||||||
responses:
|
|
||||||
'302':
|
|
||||||
description: Redirects to the namespace management page
|
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the namespace management page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'403':
|
|
||||||
description: Forbidden (not owner or not logged in)
|
|
||||||
'404':
|
|
||||||
description: Namespace not found
|
|
||||||
|
|
||||||
/namespace/{namespace_short_id}/invite:
|
|
||||||
post:
|
|
||||||
summary: Invite User to Namespace
|
|
||||||
description: Invites a user to the namespace with a specified role (owners only).
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: namespace_short_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The short ID of the namespace
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
format: email
|
|
||||||
role:
|
|
||||||
type: string
|
|
||||||
enum: ['owner', 'editor', 'reader']
|
|
||||||
required:
|
|
||||||
- email
|
|
||||||
- role
|
|
||||||
encoding:
|
|
||||||
email:
|
|
||||||
contentType: text/plain
|
|
||||||
role:
|
|
||||||
contentType: text/plain
|
|
||||||
responses:
|
|
||||||
'302':
|
|
||||||
description: Redirects to the namespace management page
|
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the namespace management page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., invalid role)
|
|
||||||
'403':
|
|
||||||
description: Forbidden (not owner or not logged in)
|
|
||||||
'404':
|
|
||||||
description: Namespace not found
|
|
||||||
|
|
||||||
/namespace/{namespace_short_id}/remove_user:
|
|
||||||
post:
|
|
||||||
summary: Remove User from Namespace
|
|
||||||
description: Removes a user from the namespace (owners only, cannot remove self).
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: namespace_short_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The short ID of the namespace
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
user_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
encoding:
|
|
||||||
user_id:
|
|
||||||
contentType: text/plain
|
|
||||||
responses:
|
|
||||||
'302':
|
|
||||||
description: Redirects to the namespace management page
|
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the namespace management page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., cannot remove self)
|
|
||||||
'403':
|
|
||||||
description: Forbidden (not owner or not logged in)
|
|
||||||
'404':
|
|
||||||
description: Namespace or user not found
|
|
||||||
|
|
||||||
/namespace/{namespace_short_id}/change_member_role:
|
|
||||||
post:
|
|
||||||
summary: Change Member Role in Namespace
|
|
||||||
description: Changes a member's role in the namespace (owners only, cannot change own role).
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: namespace_short_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The short ID of the namespace
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
user_id:
|
|
||||||
type: string
|
|
||||||
role:
|
|
||||||
type: string
|
|
||||||
enum: ['owner', 'editor', 'reader']
|
|
||||||
required:
|
|
||||||
- user_id
|
|
||||||
- role
|
|
||||||
encoding:
|
|
||||||
user_id:
|
|
||||||
contentType: text/plain
|
|
||||||
role:
|
|
||||||
contentType: text/plain
|
|
||||||
responses:
|
|
||||||
'302':
|
|
||||||
description: Redirects to the namespace management page
|
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the namespace management page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., cannot change own role)
|
|
||||||
'403':
|
|
||||||
description: Forbidden (not owner or not logged in)
|
|
||||||
'404':
|
|
||||||
description: Namespace or user not found
|
|
||||||
|
|
||||||
/namespace/{namespace_short_id}/generate_agent_jwt:
|
|
||||||
post:
|
|
||||||
summary: Generate Agent JWT
|
|
||||||
description: Generates a JWT token for an agent (owners only).
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: namespace_short_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The short ID of the namespace
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
agent_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- agent_name
|
|
||||||
encoding:
|
|
||||||
agent_name:
|
|
||||||
contentType: text/plain
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Agent JWT generated and displayed
|
|
||||||
content:
|
|
||||||
text/html:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., agent name missing)
|
|
||||||
'403':
|
|
||||||
description: Forbidden (not owner or not logged in)
|
|
||||||
'404':
|
|
||||||
description: Namespace not found
|
|
||||||
|
|
||||||
/namespace/{namespace_short_id}/revoke_agent:
|
|
||||||
post:
|
|
||||||
summary: Revoke Agent
|
|
||||||
description: Revokes an agent's access by invalidating their JWT (owners only).
|
|
||||||
security:
|
|
||||||
- sessionAuth: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: namespace_short_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The short ID of the namespace
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
agent_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- agent_id
|
|
||||||
encoding:
|
|
||||||
agent_id:
|
|
||||||
contentType: text/plain
|
|
||||||
responses:
|
|
||||||
'302':
|
|
||||||
description: Redirects to the namespace management page
|
|
||||||
headers:
|
|
||||||
Location:
|
|
||||||
description: URL of the namespace management page
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., agent ID missing)
|
|
||||||
'403':
|
|
||||||
description: Forbidden (not owner or not logged in)
|
|
||||||
'404':
|
|
||||||
description: Namespace or agent not found
|
|
||||||
|
|
||||||
/namespace/{namespace_short_id}/logs:
|
|
||||||
get:
|
|
||||||
summary: View Namespace Logs
|
|
||||||
description: |
|
|
||||||
Displays logs for the specified namespace.
|
|
||||||
- **Public Namespaces:** Accessible to all users, including guests.
|
|
||||||
- **Private Namespaces:** Requires authentication and at least 'reader' role.
|
|
||||||
security:
|
|
||||||
- {} # Allows public access to this endpoint
|
|
||||||
- sessionAuth: []
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: namespace_short_id
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: The short ID of the namespace
|
|
||||||
- in: query
|
|
||||||
name: q
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
description: Search query to filter logs
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Logs page rendered
|
|
||||||
content:
|
|
||||||
text/html:
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
'403':
|
|
||||||
description: Forbidden (user does not have access to private namespace)
|
|
||||||
'404':
|
|
||||||
description: Namespace not found
|
|
||||||
|
|
||||||
/webhook:
|
|
||||||
post:
|
|
||||||
summary: Submit Log Entry
|
|
||||||
description: Allows agents to submit log entries via JWT authentication.
|
|
||||||
security:
|
|
||||||
- agentAuth: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
description: The log message
|
|
||||||
level:
|
|
||||||
type: string
|
|
||||||
description: Log level (e.g., INFO, ERROR)
|
|
||||||
default: INFO
|
|
||||||
metadata:
|
|
||||||
type: object
|
|
||||||
description: Additional metadata for the log entry
|
|
||||||
required:
|
|
||||||
- message
|
|
||||||
responses:
|
|
||||||
'201':
|
|
||||||
description: Log entry created
|
|
||||||
'400':
|
|
||||||
description: Bad Request (e.g., message missing)
|
|
||||||
'401':
|
|
||||||
description: Unauthorized (missing or invalid JWT)
|
|
||||||
'404':
|
|
||||||
description: Namespace or agent not found
|
|
||||||
|
|
||||||
components:
|
|
||||||
securitySchemes:
|
|
||||||
sessionAuth:
|
|
||||||
type: apiKey
|
|
||||||
in: cookie
|
|
||||||
name: session
|
|
||||||
description: Session cookie for authenticated users
|
|
||||||
agentAuth:
|
|
||||||
type: http
|
|
||||||
scheme: bearer
|
|
||||||
bearerFormat: JWT
|
|
||||||
description: JWT authentication for agents
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,11 @@ pyramid_debugtoolbar
|
||||||
waitress
|
waitress
|
||||||
pyramid_retry
|
pyramid_retry
|
||||||
|
|
||||||
pyramid_jwt
|
|
||||||
|
|
||||||
pyramid_openapi3
|
|
||||||
|
|
||||||
pyramid_tm
|
pyramid_tm
|
||||||
zope.sqlalchemy
|
zope.sqlalchemy
|
||||||
|
|
||||||
bcrypt
|
bcrypt
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
werkzeug
|
werkzeug
|
||||||
|
|
||||||
|
pyramid_openapi3
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}PyraLogs{% endblock %}</title>
|
<title>{% block title %}Media Hosting App{% endblock %}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="https://www.unturf.com/css/pico.classless.min.css" rel="stylesheet">
|
<link href="https://www.unturf.com/css/pico.classless.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -39,26 +39,6 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.link-button {
|
|
||||||
width: auto;
|
|
||||||
display: inline;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--pico-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.link-button:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
form.inline-form {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hamburger Menu Button */
|
/* Hamburger Menu Button */
|
||||||
#mobile-menu-button {
|
#mobile-menu-button {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -119,21 +99,16 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="{{ request.route_url('home') }}"><strong>{{ request.domain }}</strong></a>
|
<a href="{{ request.route_url('home') }}"><strong>upload.unturf.com</strong></a>
|
||||||
<!-- Hamburger Menu Button (only visible on mobile) -->
|
<!-- Hamburger Menu Button (only visible on mobile) -->
|
||||||
<button id="mobile-menu-button" aria-label="Open Menu">☰</button>
|
<button id="mobile-menu-button" aria-label="Open Menu">☰</button>
|
||||||
<!-- Navigation Links -->
|
<!-- Navigation Links -->
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ request.route_url('home') }}">Home</a></li>
|
<li><a href="{{ request.route_url('list_media') }}">Browse Media</a></li>
|
||||||
{% if request.user and request.user.is_verified %}
|
{% if request.user and request.user.is_verified %}
|
||||||
<li><a href="{{ request.route_url('create_namespace') }}">Create Namespace</a></li>
|
<li><a href="{{ request.route_url('upload_media') }}">Upload Media</a></li>
|
||||||
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
||||||
<li>
|
<li><a href="{{ request.route_url('logout') }}">Logout</a></li>
|
||||||
<form action="{{ request.route_url('logout') }}" method="post" style="display: inline;">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
|
||||||
<button type="submit" class="link-button">Logout</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -146,16 +121,11 @@
|
||||||
<button aria-label="Close Menu" id="mobile-menu-close">✕</button>
|
<button aria-label="Close Menu" id="mobile-menu-close">✕</button>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ request.route_url('home') }}">Home</a></li>
|
<li><a href="{{ request.route_url('list_media') }}">Browse Media</a></li>
|
||||||
{% if request.user and request.user.is_verified %}
|
{% if request.user and request.user.is_verified %}
|
||||||
<li><a href="{{ request.route_url('create_namespace') }}">Create Namespace</a></li>
|
<li><a href="{{ request.route_url('upload_media') }}">Upload Media</a></li>
|
||||||
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
||||||
<li>
|
<li><a href="{{ request.route_url('logout') }}">Logout</a></li>
|
||||||
<form action="{{ request.route_url('logout') }}" method="post" style="display: inline;">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
|
||||||
<button type="submit" class="link-button">Logout</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -164,23 +134,18 @@
|
||||||
</article>
|
</article>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|
||||||
<main class="container">
|
|
||||||
<!-- Flash messages -->
|
<!-- Flash messages -->
|
||||||
{% if request.session.peek_flash() %}
|
{% if request.session.peek_flash() %}
|
||||||
{% for message in request.session.pop_flash() %}
|
{% for message in request.session.pop_flash() %}
|
||||||
<div class="alert"><mark>{{ message }}</mark></div>
|
<div class="alert">{{ message }}</div>
|
||||||
<br/>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
|
||||||
<a href="https://git.unturf.com/engineering/unturf/logs.unturf.com">PyraLogs public domain source code</a>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<!-- JavaScript for Mobile Menu -->
|
<!-- JavaScript for Mobile Menu -->
|
||||||
<script>
|
<script>
|
||||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends 'base.html.j2' %}
|
|
||||||
|
|
||||||
{% block title %}Create Namespace{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>Create a New Namespace</h1>
|
|
||||||
<form method="POST">
|
|
||||||
<label for="name">Namespace Name:</label>
|
|
||||||
<input type="text" name="name" required><br><br>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_public">
|
|
||||||
Make Namespace Public
|
|
||||||
</label><br><br>
|
|
||||||
|
|
||||||
<button type="submit">Create Namespace</button>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{% extends 'base.html.j2' %}
|
|
||||||
|
|
||||||
{% block title %}Agent JWT{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>JWT for Agent '{{ agent_name }}'</h1>
|
|
||||||
{% if message %}
|
|
||||||
<p>{{ message }}</p>
|
|
||||||
{% endif %}
|
|
||||||
<p>Please store this JWT securely. It will not be shown again.</p>
|
|
||||||
<pre>{{ jwt_token }}</pre>
|
|
||||||
<p>You can use this JWT to authenticate your agent when sending logs to the namespace '{{ namespace.name }}'.</p>
|
|
||||||
|
|
||||||
<p><a href="{{ request.route_url('manage_namespace', namespace_short_id=namespace.short_id) }}">Back to Manage Namespace</a></p>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
35
templates/edit_media.html.j2
Normal file
35
templates/edit_media.html.j2
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
|
||||||
|
{% block title %}Edit Media{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>Edit Media</h1>
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<label for="title">Title (optional):</label>
|
||||||
|
<input type="text" name="title" value="{{ media.title }}" placeholder="Enter a title for the media"><br><br>
|
||||||
|
|
||||||
|
<label for="media_file">Replace Media File (leave blank to keep current):</label>
|
||||||
|
<input type="file" name="media_file" accept="image/*,audio/*,video/*"><br><br>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="is_public" {% if media.is_public %}checked{% endif %}>
|
||||||
|
Make Media Public
|
||||||
|
</label><br><br>
|
||||||
|
|
||||||
|
<button type="submit">Update</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<!-- View and Download links -->
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=request.user.short_id, media_short_id=media.short_id) }}">View</a> |
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=request.user.short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a> |
|
||||||
|
<a href="{{ request.route_url('view_media_details', user_short_id=request.user.short_id, media_short_id=media.short_id) }}">Back to Media Details</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<form action="{{ request.route_url('delete_media', user_short_id=request.user.short_id, media_short_id=media.short_id) }}" method="post">
|
||||||
|
<button type="submit" onclick="return confirm('Are you sure you want to delete this media?');" style="border: 0px; background-color: #AA0000; color: white;">Delete</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
{% block title %}Home{% endblock %}
|
{% block title %}Home{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Welcome to PyraLogs</h1>
|
<h1>Welcome to the Media Hosting App</h1>
|
||||||
<p>
|
<p>
|
||||||
{% if request.user and request.user.is_verified %}
|
{% if request.user and request.user.is_verified %}
|
||||||
Hello, {{ request.user.username }}!
|
Hello, {{ request.user.username }}!
|
||||||
|
|
@ -11,38 +11,5 @@
|
||||||
Welcome, Guest!
|
Welcome, Guest!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p>This application allows environments and agents to centrally log events to this hub. You can create namespaces to organize logs and control access.</p>
|
<p>This application allows authenticated users to upload images, audio, and short videos. Guests can browse and download publicly available media.</p>
|
||||||
|
|
||||||
{% if request.user and request.user.is_verified %}
|
|
||||||
<h2>Your Namespaces</h2>
|
|
||||||
{% if user_namespaces %}
|
|
||||||
<ul>
|
|
||||||
{% for ns in user_namespaces %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ request.route_url('view_namespace_logs', namespace_short_id=ns.short_id) }}">{{ ns.name }} logs</a>
|
|
||||||
{% if ns.role == 'editor' or ns.role == 'owner' %}
|
|
||||||
| <a href="{{ request.route_url('manage_namespace', namespace_short_id=ns.short_id) }}">Manage</a>
|
|
||||||
{% endif %}
|
|
||||||
<br>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<p>You are not a member of any namespaces.</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2>Public Namespaces</h2>
|
|
||||||
{% if public_namespaces %}
|
|
||||||
<ul>
|
|
||||||
{% for ns in public_namespaces %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ request.route_url('view_namespace_logs', namespace_short_id=ns.short_id) }}">{{ ns.name }} logs</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<p>No public namespaces available.</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
12
templates/import_user_record.html.j2
Normal file
12
templates/import_user_record.html.j2
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
|
||||||
|
{% block title %}Import User Record{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Import User Record</h1>
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<label for="user_record_file">Select User Record JSON File:</label>
|
||||||
|
<input type="file" name="user_record_file" accept="application/json" required><br><br>
|
||||||
|
<button type="submit">Import</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
30
templates/list_media.html.j2
Normal file
30
templates/list_media.html.j2
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
|
||||||
|
{% block title %}Media List{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Media List</h1>
|
||||||
|
|
||||||
|
{% if media_list %}
|
||||||
|
<ul>
|
||||||
|
{% for media_item in media_list %}
|
||||||
|
{% set media = media_item.media %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ media.title if media.title else media.filename }}</strong><br>
|
||||||
|
Uploaded by: <a href="{{ request.route_url('user_media', user_short_id=media_item.user_short_id) }}">{{ media_item.username }}</a><br>
|
||||||
|
<!-- View and Download links -->
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=media_item.user_short_id, media_short_id=media.short_id) }}">View</a> |
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=media_item.user_short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a> |
|
||||||
|
<!-- Link to media detail page -->
|
||||||
|
<a href="{{ request.route_url('view_media_details', user_short_id=media_item.user_short_id, media_short_id=media.short_id) }}">Details</a>
|
||||||
|
{% if request.user and request.user.id == media.user_id %}
|
||||||
|
| <a href="{{ request.route_url('edit_media', user_short_id=media_item.user_short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>No media found.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
{% extends 'base.html.j2' %}
|
|
||||||
|
|
||||||
{% block title %}Manage Namespace{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>Manage Namespace: {{ namespace.name }}</h1>
|
|
||||||
<form method="POST" action="{{ request.route_url('update_namespace', namespace_short_id=namespace.short_id) }}">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="is_public" {% if namespace.is_public %}checked{% endif %}>
|
|
||||||
Make Namespace Public
|
|
||||||
</label><br><br>
|
|
||||||
|
|
||||||
<button type="submit">Update Namespace</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Only owners can see the user management section -->
|
|
||||||
{% if request.user_namespace_role == 'owner' %}
|
|
||||||
|
|
||||||
<h2>Users in Namespace</h2>
|
|
||||||
{% for member in users %}
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{{ member.user.username }}
|
|
||||||
{% if request.user.id != member.user.id %}
|
|
||||||
<!-- Form to remove user -->
|
|
||||||
<form action="{{ request.route_url('remove_user', namespace_short_id=namespace.short_id) }}" method="post" class="inline-form">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
|
||||||
<input type="hidden" name="user_id" value="{{ member.user.id }}">
|
|
||||||
<button type="submit" class="link-button">Remove</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{% if request.user.id != member.user.id %}
|
|
||||||
<!-- Form to change role -->
|
|
||||||
<form action="{{ request.route_url('change_member_role', namespace_short_id=namespace.short_id) }}" method="post" class="inline-form">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
|
||||||
<input type="hidden" name="user_id" value="{{ member.user.id }}">
|
|
||||||
<select name="role">
|
|
||||||
<option value="owner" {% if member.role == 'owner' %}selected{% endif %}>Owner</option>
|
|
||||||
<option value="editor" {% if member.role == 'editor' %}selected{% endif %}>Editor</option>
|
|
||||||
<option value="reader" {% if member.role == 'reader' %}selected{% endif %}>Reader</option>
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="link-button">Change Role</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
{{ member.role }}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<h2>Invite User</h2>
|
|
||||||
<form method="POST" action="{{ request.route_url('invite_user', namespace_short_id=namespace.short_id) }}">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
|
||||||
<label for="email">User Email:</label>
|
|
||||||
<input type="email" name="email" required><br><br>
|
|
||||||
|
|
||||||
<label for="role">Role:</label>
|
|
||||||
<select name="role" required>
|
|
||||||
<option value="owner">Owner</option>
|
|
||||||
<option value="editor">Editor</option>
|
|
||||||
<option value="reader">Reader</option>
|
|
||||||
</select><br><br>
|
|
||||||
|
|
||||||
<button type="submit">Invite User</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
<!-- Only owners can see the user management section -->
|
|
||||||
|
|
||||||
<h2>Agents</h2>
|
|
||||||
{% if agents %}
|
|
||||||
<ul>
|
|
||||||
{% for agent in agents %}
|
|
||||||
<li>
|
|
||||||
{{ agent.name }} ({{ agent.status|capitalize }})
|
|
||||||
<form method="POST" action="{{ request.route_url('revoke_agent', namespace_short_id=namespace.short_id) }}" class="inline-form">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
|
||||||
<input type="hidden" name="agent_id" value="{{ agent.id }}">
|
|
||||||
<button type="submit" class="link-button" onclick="return confirm('Revoke access for {{ agent.name }}?')">Revoke Access</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<p>No agents found.</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2>Generate JWT for Agent</h2>
|
|
||||||
<form method="POST" action="{{ request.route_url('generate_agent_jwt', namespace_short_id=namespace.short_id) }}">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
|
||||||
<label for="agent_name">Agent Name:</label>
|
|
||||||
<input type="text" name="agent_name" required><br><br>
|
|
||||||
|
|
||||||
<button type="submit">Generate JWT</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -23,5 +23,15 @@
|
||||||
<button type="submit">Update Profile</button>
|
<button type="submit">Update Profile</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2>Your Stats</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Total Uploads: {{ total_uploads }}</li>
|
||||||
|
<li>Total Storage Used: {{ total_size | filesizeformat }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Your Media</h2>
|
||||||
|
<p><a href="{{ request.route_url('user_media', user_short_id=user.short_id) }}">View Your Uploads</a></p>
|
||||||
|
<p><a href="{{ request.route_url('upload_media') }}">Upload New Media</a></p>
|
||||||
|
<p><a href="{{ request.route_url('download_database') }}">Download Your Database</a></p>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
22
templates/upload_media.html.j2
Normal file
22
templates/upload_media.html.j2
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
|
||||||
|
{% block title %}Upload Media{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Upload Media</h1>
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<label for="media_file">Select Media File (Max 30MB):</label>
|
||||||
|
<input type="file" name="media_file" accept="image/*,audio/*,video/*" required><br><br>
|
||||||
|
|
||||||
|
<label for="title">Title (optional):</label>
|
||||||
|
<input type="text" name="title" placeholder="Enter a title for the media"><br><br>
|
||||||
|
|
||||||
|
<!-- Optionally, allow users to set media as public or private -->
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="is_public" checked>
|
||||||
|
Make Media Public
|
||||||
|
</label><br><br>
|
||||||
|
|
||||||
|
<button type="submit">Upload</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
33
templates/user_media.html.j2
Normal file
33
templates/user_media.html.j2
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
|
||||||
|
{% block title %}{{ user.username }}'s Media{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ user.username }}'s Media</h1>
|
||||||
|
|
||||||
|
{% if media_items %}
|
||||||
|
<ul>
|
||||||
|
{% for media in media_items %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ media.title if media.title else media.filename }}</strong><br>
|
||||||
|
{% if media.is_public %}
|
||||||
|
<span>Status: Public</span>
|
||||||
|
{% else %}
|
||||||
|
<span>Status: Private</span>
|
||||||
|
{% endif %}<br>
|
||||||
|
<!-- View and Download links -->
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=user.short_id, media_short_id=media.short_id) }}">View</a> |
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=user.short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a> |
|
||||||
|
<!-- Link to media detail page -->
|
||||||
|
<a href="{{ request.route_url('view_media_details', user_short_id=user.short_id, media_short_id=media.short_id) }}">Details</a>
|
||||||
|
{% if request.user and request.user.id == media.user_id %}
|
||||||
|
| <a href="{{ request.route_url('edit_media', user_short_id=user.short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>No media found.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
38
templates/view_media.html.j2
Normal file
38
templates/view_media.html.j2
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
|
||||||
|
{% block title %}View Media{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ media.title if media.title else media.filename }}</h1>
|
||||||
|
<p>Uploaded by <a href="{{ request.route_url('user_media', user_short_id=user_short_id) }}">{{ username }}</a> on {{ media.upload_date.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||||
|
<p>Size: {{ media.size | filesizeformat }}</p>
|
||||||
|
|
||||||
|
<!-- Display media based on type -->
|
||||||
|
{% if media.media_type == 'image' %}
|
||||||
|
<img src="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}" alt="{{ media.filename }}">
|
||||||
|
{% elif media.media_type == 'audio' %}
|
||||||
|
<audio controls>
|
||||||
|
<source src="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">
|
||||||
|
Your browser does not support the audio element.
|
||||||
|
</audio>
|
||||||
|
{% elif media.media_type == 'video' %}
|
||||||
|
<video controls width="600">
|
||||||
|
<source src="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- View and Download links -->
|
||||||
|
<p>
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">View</a> |
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if is_owner %}
|
||||||
|
<!-- Edit button for the owner -->
|
||||||
|
<p>
|
||||||
|
<a href="{{ request.route_url('edit_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
24
templates/view_media_details.html.j2
Normal file
24
templates/view_media_details.html.j2
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
|
||||||
|
{% block title %}Media Details{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Media Details</h1>
|
||||||
|
|
||||||
|
<p><strong>Title:</strong> {{ media.title if media.title else 'Untitled' }}</p>
|
||||||
|
<p><strong>Filename:</strong> {{ media.filename }}</p>
|
||||||
|
<p><strong>Uploaded by:</strong> <a href="{{ request.route_url('user_media', user_short_id=user_short_id) }}">{{ username }}</a></p>
|
||||||
|
<p><strong>Upload Date:</strong> {{ media.upload_date.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||||
|
<p><strong>Status:</strong> {{ 'Public' if media.is_public else 'Private' }}</p>
|
||||||
|
<p><strong>Size:</strong> {{ media.size | filesizeformat }}</p>
|
||||||
|
|
||||||
|
<!-- View and Download links -->
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">View</a> |
|
||||||
|
<a href="{{ request.route_url('view_media', user_short_id=user_short_id, media_short_id=media.short_id, _query={'download': 'true'}) }}">Download</a>
|
||||||
|
|
||||||
|
{% if is_owner %}
|
||||||
|
<!-- Edit button for the owner -->
|
||||||
|
| <a href="{{ request.route_url('edit_media', user_short_id=user_short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
{% extends 'base.html.j2' %}
|
|
||||||
|
|
||||||
{% block title %}Logs for {{ namespace.name }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>Logs for Namespace: {{ namespace.name }}</h1>
|
|
||||||
|
|
||||||
<!-- Search Form -->
|
|
||||||
<form method="get" action="{{ request.current_route_url() }}">
|
|
||||||
<input type="text" name="q" value="{{ search_query }}" placeholder="Search logs">
|
|
||||||
<button type="submit">Search</button>
|
|
||||||
{% if search_query %}
|
|
||||||
<a href="{{ request.route_url('view_namespace_logs', namespace_short_id=namespace.short_id) }}">Clear Search</a>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Logs Table -->
|
|
||||||
<div class="logs-container">
|
|
||||||
{% if logs %}
|
|
||||||
<table class="logs-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Timestamp</th>
|
|
||||||
<th>Level</th>
|
|
||||||
<th>Message</th>
|
|
||||||
<th>Metadata</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for log in logs %}
|
|
||||||
<tr class="log-entry">
|
|
||||||
<td class="log-timestamp">{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
|
||||||
<td class="log-level">{{ log.level }}</td>
|
|
||||||
<td class="log-message">{{ log.message }}</td>
|
|
||||||
<td class="log-metadata">
|
|
||||||
{% if log.log_metadata %}
|
|
||||||
<pre>{{ log.log_metadata }}</pre>
|
|
||||||
{% else %}
|
|
||||||
N/A
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<p>No logs found.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Include JavaScript for highlighting -->
|
|
||||||
{% if search_query %}
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
// Function to escape special characters in regex
|
|
||||||
function escapeRegExp(string) {
|
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchQuery = {{ search_query|tojson }};
|
|
||||||
|
|
||||||
if (!searchQuery) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var regex = new RegExp(escapeRegExp(searchQuery), 'gi');
|
|
||||||
|
|
||||||
// Function to highlight text
|
|
||||||
function highlightText(element) {
|
|
||||||
element.childNodes.forEach(function(node) {
|
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.parentNode.nodeName !== 'MARK') {
|
|
||||||
var text = node.textContent;
|
|
||||||
var html = text.replace(regex, function(match) {
|
|
||||||
return '<mark>' + match + '</mark>';
|
|
||||||
});
|
|
||||||
if (html !== text) {
|
|
||||||
var temp = document.createElement('span');
|
|
||||||
temp.innerHTML = html;
|
|
||||||
// Replace the text node with the new HTML
|
|
||||||
var fragment = document.createDocumentFragment();
|
|
||||||
while (temp.firstChild) {
|
|
||||||
fragment.appendChild(temp.firstChild);
|
|
||||||
}
|
|
||||||
node.parentNode.replaceChild(fragment, node);
|
|
||||||
}
|
|
||||||
} else if (node.nodeType === Node.ELEMENT_NODE && node.nodeName !== 'SCRIPT') {
|
|
||||||
highlightText(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlight search terms in log entries
|
|
||||||
document.querySelectorAll('.log-entry td').forEach(function(element) {
|
|
||||||
highlightText(element);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
166
test_agent.sh
166
test_agent.sh
|
|
@ -1,34 +1,150 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# A Bash script to demonstrate and test multiple authenticated routes
|
||||||
|
# in the pyrafiles application (or a similar Pyramid-based app).
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Checks if cookies.txt is older than a defined threshold (default 1 year).
|
||||||
|
# - If too old, it deletes cookies.txt so we force re-login.
|
||||||
|
# 2. If cookies.txt is still valid or absent, proceeds to the next steps:
|
||||||
|
# - If cookies.txt does not exist or was removed, it does the email + OTP flow.
|
||||||
|
# - If cookies.txt exists and is fresh, it reuses that session.
|
||||||
|
# 3. Fetches and prints the Profile page (GET /auth/profile) as a quick test.
|
||||||
|
# 4. Uploads a sample media file (POST /media/upload), marked public.
|
||||||
|
# 5. Parses the server's redirect "Location" header to extract user_short_id and media_short_id.
|
||||||
|
# 6. Views media details (GET /media/<user_short_id>/<media_short_id>/details).
|
||||||
|
# 7. Edits the newly uploaded media (POST /media/<user_short_id>/<media_short_id>/edit).
|
||||||
|
# 8. Deletes the media (POST /media/<user_short_id>/<media_short_id>/delete).
|
||||||
|
# 9. Lists all public media (GET /media/list).
|
||||||
|
# 10. Logs out (GET /auth/logout).
|
||||||
|
#
|
||||||
|
# The script exits on the first command error (set -e).
|
||||||
|
# Adjust BASE_URL, EMAIL, and file paths as needed.
|
||||||
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./test_agent.sh "Your log message"
|
# MAX_COOKIE_AGE=<seconds> ./test_all_routes.sh /path/to/somefile.jpg
|
||||||
|
# By default, MAX_COOKIE_AGE=31536000 (1 year).
|
||||||
|
# If cookies.txt is younger than that, we skip re-login. Otherwise, we do the OTP flow.
|
||||||
|
#
|
||||||
|
# By default, uses http://localhost:6544 and agent@example.com
|
||||||
|
|
||||||
# Description:
|
set -e # Exit on first error
|
||||||
# Sends a log message to the PyraLogs server using the provided JWT token.
|
|
||||||
# Adjust the BASE_URL and JWT_TOKEN variables as needed.
|
|
||||||
|
|
||||||
# Variables
|
BASE_URL="${BASE_URL:-http://localhost:6544}"
|
||||||
BASE_URL="http://127.0.0.1:6544"
|
EMAIL="${EMAIL:-agent@example.com}"
|
||||||
JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZ2VudF9pZCI6IjcxZmQ0ZjA0LTQ2NzgtNGEyNi05ZmU4LTkxNjU2OTNmOTBmOSIsImFnZW50X25hbWUiOiJ0ZXN0LTkwMDAiLCJuYW1lc3BhY2VfaWQiOiJhZGQwYWQ1YS0zNjE0LTRhODgtOTRmZC0xNGQ4ZDBkYjIxYjUiLCJ0b2tlbl92ZXJzaW9uIjo1LCJpYXQiOjE3MzY2ODIxMTV9.p2HQmjFBtL9XLePtVCu3HyPhw89993BO4b6E82SwyEM"
|
MEDIA_FILE="$1"
|
||||||
|
MAX_COOKIE_AGE="${MAX_COOKIE_AGE:-31536000}" # default ~1 year in seconds
|
||||||
|
|
||||||
# Log message from the first argument
|
if [[ -z "$MEDIA_FILE" ]]; then
|
||||||
LOG_MESSAGE="$1"
|
echo "Usage: MAX_COOKIE_AGE=<seconds> $0 /path/to/mediafile"
|
||||||
|
|
||||||
# Check if a log message was provided
|
|
||||||
if [[ -z "$LOG_MESSAGE" ]]; then
|
|
||||||
echo "Usage: $0 \"Your log message\""
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Send the log message using curl
|
echo "=== Checking cookies.txt freshness ==="
|
||||||
RESPONSE=$(curl -s -X POST "$BASE_URL/webhook" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer $JWT_TOKEN" \
|
|
||||||
-d "{
|
|
||||||
\"message\": \"$LOG_MESSAGE\",
|
|
||||||
\"level\": \"INFO\",
|
|
||||||
\"metadata\": {\"w\": 1}
|
|
||||||
}")
|
|
||||||
|
|
||||||
# Output the server's response
|
COOKIE_FRESH=0
|
||||||
echo "Server Response: $RESPONSE"
|
if [[ -f cookies.txt ]]; then
|
||||||
|
# 'stat -c %Y' returns the file modification time on Linux
|
||||||
|
# 'stat -f %m' does the same on BSD/macOS
|
||||||
|
MOD_TIME=$(stat -c %Y cookies.txt 2>/dev/null || stat -f %m cookies.txt 2>/dev/null)
|
||||||
|
NOW=$(date +%s)
|
||||||
|
AGE=$((NOW - MOD_TIME))
|
||||||
|
|
||||||
|
echo "cookies.txt is $AGE seconds old, threshold is $MAX_COOKIE_AGE."
|
||||||
|
if (( AGE < MAX_COOKIE_AGE )); then
|
||||||
|
COOKIE_FRESH=1
|
||||||
|
echo "Reusing existing cookies.txt (fresh enough)."
|
||||||
|
else
|
||||||
|
echo "cookies.txt is too old. Removing it to force re-login."
|
||||||
|
rm -f cookies.txt
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No cookies.txt found. A new session will be created."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$COOKIE_FRESH" -eq 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "=== Starting new session and logging in with OTP flow ==="
|
||||||
|
curl -s -i -c cookies.txt -b cookies.txt "$BASE_URL/" >/dev/null
|
||||||
|
|
||||||
|
echo "Logging in with email: $EMAIL (POST /auth/login)"
|
||||||
|
curl -s -i -c cookies.txt -b cookies.txt \
|
||||||
|
-X POST \
|
||||||
|
-F "email=$EMAIL" \
|
||||||
|
"$BASE_URL/auth/login"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Check your server logs or email for the 6-digit verification code."
|
||||||
|
read -p "Enter the 6-digit code: " VERIFICATION_CODE
|
||||||
|
|
||||||
|
echo "Verifying code (POST /auth/verify)"
|
||||||
|
curl -s -i -c cookies.txt -b cookies.txt \
|
||||||
|
-X POST \
|
||||||
|
-F "code=$VERIFICATION_CODE" \
|
||||||
|
"$BASE_URL/auth/verify"
|
||||||
|
|
||||||
|
echo "Login/OTP flow complete. cookies.txt saved."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Quick check: Fetching Profile page (GET /auth/profile) ==="
|
||||||
|
curl -i -c cookies.txt -b cookies.txt "$BASE_URL/auth/profile"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Uploading media (POST /media/upload) as public ==="
|
||||||
|
UPLOAD_RESPONSE=$(curl -i -c cookies.txt -b cookies.txt \
|
||||||
|
-X POST \
|
||||||
|
-F "title=TestUploadByAgent" \
|
||||||
|
-F "media_file=@${MEDIA_FILE}" \
|
||||||
|
-F "is_public=on" \
|
||||||
|
"$BASE_URL/media/upload")
|
||||||
|
|
||||||
|
echo "$UPLOAD_RESPONSE"
|
||||||
|
echo ""
|
||||||
|
echo "Parsing 'Location' header from upload response..."
|
||||||
|
|
||||||
|
LOCATION_HEADER=$(echo "$UPLOAD_RESPONSE" | grep -i "^Location:" | head -n1 | sed 's/\r//g')
|
||||||
|
LOCATION_URL="${LOCATION_HEADER#Location: }"
|
||||||
|
LOCATION_URL=$(echo "$LOCATION_URL" | tr -d '[:space:]')
|
||||||
|
LOCATION_PATH="${LOCATION_URL#*//*/}"
|
||||||
|
|
||||||
|
USER_SHORT_ID=$(echo "$LOCATION_PATH" | cut -d'/' -f2)
|
||||||
|
MEDIA_SHORT_ID=$(echo "$LOCATION_PATH" | cut -d'/' -f3)
|
||||||
|
|
||||||
|
echo "user_short_id=$USER_SHORT_ID"
|
||||||
|
echo "media_short_id=$MEDIA_SHORT_ID"
|
||||||
|
|
||||||
|
if [[ -z "$USER_SHORT_ID" || -z "$MEDIA_SHORT_ID" ]]; then
|
||||||
|
echo "Failed to parse user or media short ID. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Viewing media details (GET /media/$USER_SHORT_ID/$MEDIA_SHORT_ID/details) ==="
|
||||||
|
curl -i -c cookies.txt -b cookies.txt \
|
||||||
|
"$BASE_URL/media/$USER_SHORT_ID/$MEDIA_SHORT_ID/details"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Editing media title to 'RenamedByAgent' (POST /media/$USER_SHORT_ID/$MEDIA_SHORT_ID/edit) ==="
|
||||||
|
curl -i -c cookies.txt -b cookies.txt \
|
||||||
|
-X POST \
|
||||||
|
-F "title=RenamedByAgent" \
|
||||||
|
"$BASE_URL/media/$USER_SHORT_ID/$MEDIA_SHORT_ID/edit"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Deleting the media (POST /media/$USER_SHORT_ID/$MEDIA_SHORT_ID/delete) ==="
|
||||||
|
curl -i -c cookies.txt -b cookies.txt \
|
||||||
|
-X POST \
|
||||||
|
"$BASE_URL/media/$USER_SHORT_ID/$MEDIA_SHORT_ID/delete"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Listing all public media (GET /media/list) ==="
|
||||||
|
curl -i -c cookies.txt -b cookies.txt "$BASE_URL/media/list"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Logging out (GET /auth/logout) ==="
|
||||||
|
curl -i -c cookies.txt -b cookies.txt "$BASE_URL/auth/logout"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Test complete. We exercised multiple routes, including login/verify, profile, "
|
||||||
|
echo "upload/delete/edit media, and logout. If no errors appeared, all requests succeeded!"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue