JWT and complete RBAC
This commit is contained in:
parent
7a2a2133cf
commit
5c226f2a2e
16 changed files with 1803 additions and 783 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@
|
|||
env/
|
||||
__pycache__/
|
||||
cookies.txt
|
||||
data/
|
||||
|
|
|
|||
271
README.rst
271
README.rst
|
|
@ -1,18 +1,25 @@
|
|||
===========================================
|
||||
pyrafiles - A Sophisticated Pyramid Project
|
||||
PyraFiles - A Sophisticated Pyramid Project
|
||||
===========================================
|
||||
|
||||
``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.
|
||||
**PyraFiles** is a **public domain** application built on the `Pyramid <https://trypyramid.com>`_ framework. It demonstrates a flexible, multi-database design where each namespace (group of users and agents) maintains its own SQLite database for media uploads (images, audio, video). The main database (``main.db``) stores system-level data, including user accounts, namespaces, agents, and roles.
|
||||
|
||||
PyraFiles supports both human users and agents:
|
||||
|
||||
- **Users** authenticate via OTP (One-Time Password) email verification codes and interact through the web interface.
|
||||
- **Agents** authenticate via JWT tokens and can interact programmatically with the application.
|
||||
|
||||
Key Features
|
||||
============
|
||||
|
||||
- **Passwordless Login** using email verification codes.
|
||||
- **Guest Sessions** for unverified visitors.
|
||||
- **Per-user SQLite** databases for media uploads (limit ~30MB each).
|
||||
- **Public/Private** media visibility controls.
|
||||
- **Admin Tools** (import user records, manage site settings).
|
||||
- **Agent-friendly** architecture: easily scriptable endpoints for login, verification, media uploads, etc.
|
||||
- **Passwordless Login** using email verification codes (OTP).
|
||||
- **Namespaces** to organize media files and control access.
|
||||
- **Role-Based Access Control** with roles: *owner*, *editor*, and *reader*.
|
||||
- **JWT Authentication** for agents, enabling programmatic access.
|
||||
- **Per-Namespace SQLite Databases** for media uploads.
|
||||
- **Public/Private** namespace and media visibility controls.
|
||||
- **Admin Tools** for namespace and agent management.
|
||||
- **OpenAPI Documentation** available for API interactions.
|
||||
- **Configurable** via environment variables (including secret keys).
|
||||
|
||||
Git Repository
|
||||
|
|
@ -20,22 +27,25 @@ Git Repository
|
|||
|
||||
The project is maintained at:
|
||||
|
||||
- `pyrafiles Git Repo <https://git.unturf.com/engineering/unturf/upload.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.
|
||||
|
||||
|
||||
Configuration via Environment
|
||||
=============================
|
||||
|
||||
`pyrafiles` fetches settings from environment variables with sensible defaults:
|
||||
PyraFiles fetches settings from environment variables with sensible defaults:
|
||||
|
||||
- ``PYRAFILES_SECRET``
|
||||
The secret key for session signing.
|
||||
If missing, ``pyrafiles`` automatically generates a **random 64-character** string at runtime & will log out all users out.
|
||||
If missing, PyraFiles automatically generates a **random 64-character** string at runtime, which will log out all users.
|
||||
|
||||
- ``PYRAFILES_JWT_SECRET``
|
||||
The secret key used for signing JWT tokens for agents.
|
||||
If missing, PyraFiles automatically generates a **random 64-character** string at runtime, which will invalidate existing agent tokens.
|
||||
|
||||
- ``PYRAFILES_DB_URL``
|
||||
Connection string for the main database. Default: ``sqlite:///main.db``.
|
||||
Connection string for the main database. Default: ``sqlite:///data/main.db``.
|
||||
|
||||
- ``PYRAFILES_HOST`` and ``PYRAFILES_PORT``
|
||||
The host and port to serve on. Defaults: ``0.0.0.0`` (host), ``6544`` (port).
|
||||
|
|
@ -45,7 +55,6 @@ Configuration via Environment
|
|||
|
||||
- Any other environment variables you wish to incorporate can be accessed in the code.
|
||||
|
||||
|
||||
Local Setup
|
||||
===========
|
||||
|
||||
|
|
@ -60,7 +69,7 @@ Local Setup
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m venv venv
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# or
|
||||
venv\Scripts\activate.bat # Windows
|
||||
|
|
@ -71,136 +80,156 @@ Local Setup
|
|||
|
||||
pip install -r requirements.txt
|
||||
|
||||
4. **Run**
|
||||
|
||||
Create the database if this is the first time running the applicaiton.
|
||||
4. **Initialize the Database**
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python initialize_db.py
|
||||
|
||||
Otherwise:
|
||||
This creates or updates the main database file (``data/main.db``) and ensures all tables are set up.
|
||||
|
||||
5. **Run the Application**
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Optionally set PYRAFILES_SECRET if you want a custom secret.
|
||||
# Optionally set PYRAFILES_SECRET and PYRAFILES_JWT_SECRET if you want custom secrets.
|
||||
export PYRAFILES_SECRET="YOUR_OWN_LONG_RANDOM_STRING"
|
||||
python main.py
|
||||
export PYRAFILES_JWT_SECRET="YOUR_OWN_LONG_RANDOM_STRING"
|
||||
python app.py
|
||||
|
||||
If ``PYRAFILES_SECRET`` is **not** set, the app automatically generates a 64-char secret at runtime & log out all users.
|
||||
If ``PYRAFILES_SECRET`` and ``PYRAFILES_JWT_SECRET`` are **not** set, the app automatically generates 64-character secrets at runtime, which will log out all users and invalidate existing JWT tokens.
|
||||
|
||||
5. **Access**
|
||||
6. **Access the Application**
|
||||
|
||||
Point your browser to `http://localhost:6544` or `http://<HOST>:<PORT>` according to your environment variables.
|
||||
|
||||
OTP Authentication
|
||||
==================
|
||||
|
||||
Example: Agent Workflow Script
|
||||
==============================
|
||||
PyraFiles uses a passwordless login system for users:
|
||||
|
||||
First of all, everything is documented as OpenAPI for agentic flows.
|
||||
1. **Login with Email**
|
||||
|
||||
* http://localhost:6544/docs
|
||||
* http://localhost:6544/openapi.yaml
|
||||
- Users enter their email address on the login page.
|
||||
- A 6-digit verification code is sent to the provided email address.
|
||||
|
||||
Below is a sample Bash script showing how an **agent** might:
|
||||
2. **Verify with Code**
|
||||
|
||||
1. Start the session (to get a cookie).
|
||||
2. Log in with an email.
|
||||
3. Prompt the user for a verification code.
|
||||
4. Complete the verification.
|
||||
5. Upload a file.
|
||||
- Users enter the 6-digit code on the verification page.
|
||||
- Upon successful verification, the user is logged in.
|
||||
|
||||
3. **Session Management**
|
||||
|
||||
- User sessions are managed via signed cookies.
|
||||
- Sessions persist across browser restarts unless the server secret changes.
|
||||
|
||||
JWT Agent Authentication
|
||||
========================
|
||||
|
||||
Agents authenticate with PyraFiles using JWT tokens, allowing programmatic interaction:
|
||||
|
||||
1. **Generate Agent JWT**
|
||||
|
||||
- **Owners** of a namespace can generate JWT tokens for agents.
|
||||
- Agents are assigned a role: *owner*, *editor*, or *reader*.
|
||||
- Each agent has a unique token, which includes their role and namespace ID.
|
||||
|
||||
2. **Authenticate with JWT**
|
||||
|
||||
- Agents include the JWT in the `Authorization` header as a Bearer token:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
Authorization: Bearer <JWT_TOKEN>
|
||||
|
||||
- The token is verified on each request, and the agent's role is used for access control.
|
||||
|
||||
3. **Access Control**
|
||||
|
||||
- **Owner** agents can manage the namespace, including inviting users and creating other agents.
|
||||
- **Editor** agents can upload, edit, and delete media within the namespace.
|
||||
- **Reader** agents can view media if they have appropriate permissions.
|
||||
|
||||
4. **Token Revocation**
|
||||
|
||||
- Owners can revoke agent tokens, which invalidates the JWT.
|
||||
|
||||
Example: Agent Workflow with JWT
|
||||
================================
|
||||
|
||||
Below is a sample Bash script showing how an **agent** might upload a media file using a JWT token.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Usage:
|
||||
# MAX_COOKIE_AGE=1800 ./agent_upload.sh /path/to/somefile.jpg
|
||||
# ./agent_upload_jwt.sh /path/to/mediafile.jpg
|
||||
#
|
||||
# Description:
|
||||
# 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:
|
||||
# 1. If not set, MAX_COOKIE_AGE defaults to 31536000 (one year).
|
||||
# 2. We assume the server runs on http://localhost:6544. Adjust BASE_URL if needed.
|
||||
# Upload a media file to the PyraFiles server as an agent using a JWT token.
|
||||
|
||||
BASE_URL="${BASE_URL:-http://localhost:6544}"
|
||||
EMAIL="${EMAIL:-agent@example.com}"
|
||||
JWT_TOKEN="${JWT_TOKEN:-your_agent_jwt_token}"
|
||||
NAMESPACE_SHORT_ID="${NAMESPACE_SHORT_ID:-your_namespace_short_id}"
|
||||
MEDIA_FILE="$1"
|
||||
# Default to 1 year in seconds if not provided:
|
||||
MAX_COOKIE_AGE="${MAX_COOKIE_AGE:-31536000}"
|
||||
|
||||
if [[ -z "$MEDIA_FILE" ]]; then
|
||||
echo "Usage: MAX_COOKIE_AGE=<seconds> $0 /path/to/mediafile"
|
||||
echo "Usage: $0 /path/to/mediafile.jpg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if cookies.txt is 'fresh enough':
|
||||
COOKIE_FRESH=0
|
||||
if [[ -f cookies.txt ]]; then
|
||||
# 'stat -c %Y' returns the file's mod time on Linux; 'stat -f %m' on macOS/BSD
|
||||
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))
|
||||
|
||||
if (( AGE < MAX_COOKIE_AGE )); then
|
||||
COOKIE_FRESH=1
|
||||
fi
|
||||
if [[ -z "$JWT_TOKEN" ]]; then
|
||||
echo "Error: JWT_TOKEN environment variable is not set."
|
||||
exit 1
|
||||
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."
|
||||
if [[ -z "$NAMESPACE_SHORT_ID" ]]; then
|
||||
echo "Error: NAMESPACE_SHORT_ID environment variable is not set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Uploading media: $MEDIA_FILE (public)"
|
||||
curl -s -c cookies.txt -b cookies.txt \
|
||||
echo "Uploading media: $MEDIA_FILE"
|
||||
curl -s \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer $JWT_TOKEN" \
|
||||
-F "media_file=@$MEDIA_FILE" \
|
||||
-F "is_public=on" \
|
||||
"$BASE_URL/media/upload"
|
||||
"$BASE_URL/namespace/$NAMESPACE_SHORT_ID/media/upload"
|
||||
|
||||
echo ""
|
||||
echo "Upload complete. File is public."
|
||||
echo "Upload complete."
|
||||
|
||||
OpenAPI Documentation
|
||||
=====================
|
||||
|
||||
PyraFiles provides comprehensive OpenAPI documentation for all endpoints, making it easier to integrate agents and other services.
|
||||
|
||||
Dockerfile with Caddy + uWSGI
|
||||
=============================
|
||||
- **Swagger UI Documentation**
|
||||
|
||||
Access the interactive API documentation at:
|
||||
|
||||
- http://localhost:6544/docs
|
||||
|
||||
- **OpenAPI Specification**
|
||||
|
||||
Download the OpenAPI YAML file:
|
||||
|
||||
- http://localhost:6544/openapi.yaml
|
||||
|
||||
Docker Deployment
|
||||
=================
|
||||
|
||||
Below is an example Dockerfile that:
|
||||
|
||||
- Uses **uWSGI** to run ``pyrafiles``.
|
||||
- Uses **uWSGI** to run PyraFiles.
|
||||
- Uses **Caddy** as a reverse proxy (and optionally HTTPS if configured).
|
||||
- Defines a **volume** for databases (so they are stored on the host).
|
||||
|
||||
.. code-block:: dockerfile
|
||||
|
||||
# Dockerfile for PyraFiles
|
||||
|
||||
###########################################################
|
||||
# Stage 1: Build the Python environment
|
||||
###########################################################
|
||||
|
|
@ -217,7 +246,6 @@ Below is an example Dockerfile that:
|
|||
###########################################################
|
||||
FROM caddy:2-alpine
|
||||
|
||||
# Install Python, pip, and uWSGI from Alpine
|
||||
RUN apk add --no-cache python3 py3-pip uwsgi-python3
|
||||
|
||||
WORKDIR /app
|
||||
|
|
@ -226,37 +254,25 @@ Below is an example Dockerfile that:
|
|||
COPY --from=builder /app /app
|
||||
|
||||
# Copy Caddyfile for the reverse proxy
|
||||
# (Assuming you have a Caddyfile in your repo root)
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
# Copy uWSGI config if desired
|
||||
# For example (assume you created uwsgi.ini in your repo):
|
||||
# [uwsgi]
|
||||
# module = main:app
|
||||
# master = true
|
||||
# processes = 4
|
||||
# socket = 127.0.0.1:8080
|
||||
# vacuum = true
|
||||
# die-on-term = true
|
||||
# Copy uWSGI configuration
|
||||
COPY uwsgi.ini /app/uwsgi.ini
|
||||
|
||||
# Environment variables (optional overrides).
|
||||
# If PYRAFILES_SECRET is empty, the app itself generates a random 64-char secret & logs all users out.
|
||||
# Set environment variables
|
||||
ENV PYRAFILES_SECRET=""
|
||||
ENV PYRAFILES_JWT_SECRET=""
|
||||
ENV PYRAFILES_DB_URL="sqlite:///data/main.db"
|
||||
ENV PYRAFILES_HOST="0.0.0.0"
|
||||
ENV PYRAFILES_PORT="6544"
|
||||
|
||||
# Expose HTTP and HTTPS
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
EXPOSE 80 443
|
||||
|
||||
# Define volume so host can persist user DBs outside the container
|
||||
# We'll store DB files in /data
|
||||
# Define volume for data
|
||||
VOLUME ["/data"]
|
||||
|
||||
# Command starts uWSGI and then starts Caddy
|
||||
# The app itself checks for PYRAFILES_SECRET and auto-generates one if missing.
|
||||
# Start uWSGI and Caddy
|
||||
CMD ["/bin/sh", "-c", "\
|
||||
uwsgi --ini /app/uwsgi.ini & \
|
||||
caddy run --config /etc/caddy/Caddyfile \
|
||||
|
|
@ -264,45 +280,32 @@ Below is an example Dockerfile that:
|
|||
|
||||
.. note::
|
||||
|
||||
- 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 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.:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run -d \
|
||||
-p 80:80 -p 443:443 \
|
||||
-v /my/local/dbfolder:/data \
|
||||
--name pyrafiles \
|
||||
pyrafiles-image:latest
|
||||
|
||||
- With that, any user database is stored in ``/my/local/dbfolder`` on the host.
|
||||
|
||||
- The ``/data`` volume is used to persist database files.
|
||||
- Ensure you mount a volume at ``/data`` when running the container.
|
||||
- Set ``PYRAFILES_SECRET`` and ``PYRAFILES_JWT_SECRET`` to persistent values in a production environment.
|
||||
|
||||
Tips
|
||||
====
|
||||
|
||||
- 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.
|
||||
- Because the project is **public domain**, you can adapt it without restriction, removing features, adding custom logic, etc.
|
||||
|
||||
- For **production**, set up a real SMTP server or use a third-party service (e.g., Mailgun) and configure it via environment variables (``PYRAFILES_SMTP_HOST``, etc.).
|
||||
- **Persist Secrets**: In production, set ``PYRAFILES_SECRET`` and ``PYRAFILES_JWT_SECRET`` to persistent values to avoid invalidating sessions and JWT tokens.
|
||||
- **Mount Data Volume**: Mount the ``/data`` directory to persist database files and avoid data loss.
|
||||
- **OpenAPI Integration**: Use the provided OpenAPI specification to generate client code or integrate with other services.
|
||||
|
||||
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.
|
||||
|
||||
If you find `pyrafiles` 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
|
||||
===================
|
||||
|
||||
- Issues: Please open tickets at the `git.unturf.com project page <https://git.unturf.com/engineering/unturf/upload.unturf.com>`_.
|
||||
- For general inquiries, you can reach out to the maintainers directly.
|
||||
- **Issues**: Please open tickets at the `PyraFiles Git Repo <https://git.unturf.com/engineering/unturf/upload.unturf.com>`_.
|
||||
- **Contributions**: If you register an account, let us know, and we will grant you developer access to contribute.
|
||||
- **General Inquiries**: Reach out to the maintainers directly.
|
||||
|
||||
We hope `pyrafiles` helps you get up and running quickly with a flexible media-sharing and multi-DB infrastructure!
|
||||
|
||||
If you register an account, let me know and I will bless you as an developer to contribute.
|
||||
We hope PyraFiles helps you get up and running quickly with a flexible media-sharing and multi-database infrastructure!
|
||||
|
||||
Enjoy and happy building!
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
Initialize or upgrade the main database for pyrafiles.
|
||||
Initialize or upgrade the main database for PyraFiles.
|
||||
|
||||
Usage:
|
||||
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.
|
||||
The SQLAlchemy database URL (e.g., "sqlite:///data/main.db").
|
||||
Defaults to "sqlite:///data/main.db" if not set.
|
||||
|
||||
Description:
|
||||
Creates or updates all tables referenced by `Base.metadata`.
|
||||
|
|
@ -24,11 +24,8 @@ from sqlalchemy import create_engine
|
|||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
# Adjust these imports to your actual project structure:
|
||||
# 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
|
||||
# Import Base and DATA_DIR from your app.py
|
||||
from app import Base, DATA_DIR # Adjust the import if needed
|
||||
|
||||
def usage():
|
||||
script = os.path.basename(sys.argv[0])
|
||||
|
|
@ -39,12 +36,19 @@ def usage():
|
|||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
if len(sys.argv) > 2:
|
||||
# We only expect optional arguments. If needed, parse them here.
|
||||
if len(sys.argv) > 1:
|
||||
# We do not expect any arguments
|
||||
usage()
|
||||
|
||||
# Read environment variable for DB URL
|
||||
db_url = os.environ.get("PYRAFILES_DB_URL", "sqlite:///main.db")
|
||||
# Ensure DATA_DIR exists
|
||||
if not os.path.exists(DATA_DIR):
|
||||
os.makedirs(DATA_DIR)
|
||||
logging.info(f"Created data directory at {DATA_DIR}")
|
||||
|
||||
# Set default database URL to use the data directory
|
||||
default_db_url = f"sqlite:///{os.path.join(DATA_DIR, 'main.db')}"
|
||||
# Read environment variable for DB URL or use default
|
||||
db_url = os.environ.get("PYRAFILES_DB_URL", default_db_url)
|
||||
logging.info(f"Using DB URL: {db_url}")
|
||||
|
||||
# Set up engine
|
||||
|
|
@ -65,4 +69,3 @@ def main():
|
|||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
|
|
|||
638
openapi.yaml
638
openapi.yaml
|
|
@ -4,22 +4,23 @@ info:
|
|||
version: 1.0.0
|
||||
description: |
|
||||
API specification for the PyraFiles application.
|
||||
PyraFiles allows users to register, authenticate, upload media files,
|
||||
and manage their media content.
|
||||
PyraFiles allows users to register, authenticate, manage namespaces,
|
||||
generate JWT tokens for agents, and manage media files within namespaces.
|
||||
|
||||
servers:
|
||||
- url: http://localhost:{port}
|
||||
- url: http://127.0.0.1:{port}
|
||||
description: Local development server
|
||||
variables:
|
||||
port:
|
||||
default: '6544'
|
||||
- url: https://upload.unturf.com
|
||||
description: prod for humans & agents to mingle.
|
||||
- url: https://files.example.com
|
||||
description: Production server
|
||||
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
summary: Home Page
|
||||
description: Displays the home page.
|
||||
description: Displays the home page with public namespaces and user namespaces.
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
|
|
@ -57,10 +58,13 @@ paths:
|
|||
responses:
|
||||
'302':
|
||||
description: Redirects to the 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:
|
||||
get:
|
||||
|
|
@ -90,23 +94,44 @@ paths:
|
|||
responses:
|
||||
'302':
|
||||
description: Redirects to the home page upon successful verification
|
||||
headers:
|
||||
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:
|
||||
get:
|
||||
post:
|
||||
summary: Logout User
|
||||
description: Logs out the current user.
|
||||
security:
|
||||
- sessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
csrf_token:
|
||||
type: string
|
||||
responses:
|
||||
'302':
|
||||
description: Redirects to the home 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 upload stats.
|
||||
description: Shows the user's profile, including owned namespaces.
|
||||
security:
|
||||
- sessionAuth: []
|
||||
responses:
|
||||
|
|
@ -135,109 +160,408 @@ paths:
|
|||
enum: ['on']
|
||||
new_username:
|
||||
type: string
|
||||
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)
|
||||
description: Unauthorized (user not logged in)
|
||||
|
||||
/auth/download_db:
|
||||
/namespace/create:
|
||||
get:
|
||||
summary: Download User Database
|
||||
description: Allows the user to download their personal database file.
|
||||
summary: Display Namespace Creation Page
|
||||
description: Renders the form to create a new namespace.
|
||||
security:
|
||||
- sessionAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Database file downloaded
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
'403':
|
||||
description: Unauthorized (user not logged in or unverified)
|
||||
'404':
|
||||
description: Database file not found
|
||||
|
||||
/auth/export_user_record:
|
||||
get:
|
||||
summary: Export User Record
|
||||
description: Exports the user's record as a JSON file.
|
||||
security:
|
||||
- sessionAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: User record JSON file downloaded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
'403':
|
||||
description: Unauthorized (user not logged in or unverified)
|
||||
|
||||
/admin/import_user_record:
|
||||
get:
|
||||
summary: Display Import User Record Page
|
||||
description: Renders a page to import a user record (Admin only).
|
||||
security:
|
||||
- sessionAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Import user record page rendered
|
||||
description: Namespace creation page rendered
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
'403':
|
||||
description: Forbidden (user not admin)
|
||||
description: Unauthorized (user not logged in)
|
||||
post:
|
||||
summary: Import User Record
|
||||
description: Processes uploaded user record file and imports the user (Admin only).
|
||||
summary: Create Namespace
|
||||
description: Processes the namespace creation form.
|
||||
security:
|
||||
- sessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
user_record_file:
|
||||
name:
|
||||
type: string
|
||||
format: binary
|
||||
required:
|
||||
- user_record_file
|
||||
is_public:
|
||||
type: string
|
||||
enum: ['on']
|
||||
encoding:
|
||||
name:
|
||||
contentType: text/plain
|
||||
is_public:
|
||||
contentType: text/plain
|
||||
responses:
|
||||
'302':
|
||||
description: Redirects to the home page after successful import
|
||||
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 file)
|
||||
description: Bad Request (e.g., namespace name already exists)
|
||||
'403':
|
||||
description: Forbidden (user not admin)
|
||||
description: Unauthorized (user not logged in)
|
||||
|
||||
/media/upload:
|
||||
/namespace/{namespace_short_id}/manage:
|
||||
get:
|
||||
summary: Display Media Upload Page
|
||||
description: Renders the media upload form.
|
||||
summary: Manage Namespace (Owners Only)
|
||||
description: Displays the namespace management page. **Requires 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the namespace
|
||||
responses:
|
||||
'200':
|
||||
description: Upload media page rendered
|
||||
description: Namespace management page rendered
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
'403':
|
||||
description: Unauthorized (user not logged in or unverified)
|
||||
description: Forbidden (not owner or not logged in)
|
||||
'404':
|
||||
description: Namespace not found
|
||||
|
||||
/namespace/{namespace_short_id}/update:
|
||||
post:
|
||||
summary: Upload Media
|
||||
description: Processes the uploaded media file.
|
||||
summary: Update Namespace (Owners Only)
|
||||
description: Updates namespace properties. **Requires 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
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 (Owners Only)
|
||||
description: Invites a user to the namespace with a specified role. **Requires 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
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 (Owners Only)
|
||||
description: Removes a user from the namespace. **Requires 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
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 (Owners Only)
|
||||
description: Changes a member's role in the namespace. **Requires 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
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 (Owners Only)
|
||||
description: Generates a JWT token for an agent. **Requires 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
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
|
||||
agent_role:
|
||||
type: string
|
||||
enum: ['owner', 'editor', 'reader']
|
||||
required:
|
||||
- agent_name
|
||||
- agent_role
|
||||
encoding:
|
||||
agent_name:
|
||||
contentType: text/plain
|
||||
agent_role:
|
||||
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 (Owners Only)
|
||||
description: Revokes an agent's access by invalidating their JWT. **Requires 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
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}/media/upload:
|
||||
get:
|
||||
summary: Display Media Upload Page (Editors and Owners)
|
||||
description: Renders the media upload form. **Requires 'editor' or 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Media upload page rendered
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
'403':
|
||||
description: Forbidden (insufficient permissions)
|
||||
post:
|
||||
summary: Upload Media (Editors and Owners)
|
||||
description: Uploads a media file to the namespace. **Requires 'editor' or 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
|
@ -258,15 +582,32 @@ paths:
|
|||
responses:
|
||||
'302':
|
||||
description: Redirects to the media details page
|
||||
headers:
|
||||
Location:
|
||||
description: URL of the media details page
|
||||
schema:
|
||||
type: string
|
||||
'400':
|
||||
description: Bad Request (e.g., no file uploaded, unsupported media type)
|
||||
description: Bad Request (e.g., file missing)
|
||||
'403':
|
||||
description: Unauthorized (user not logged in or unverified)
|
||||
description: Forbidden (insufficient permissions)
|
||||
'404':
|
||||
description: Namespace not found
|
||||
|
||||
/media/list:
|
||||
/namespace/{namespace_short_id}/media/list:
|
||||
get:
|
||||
summary: List Public Media
|
||||
description: Displays a list of public media from all users.
|
||||
summary: List Media in Namespace
|
||||
description: Lists all media files in the namespace.
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
- {} # Allow public access if namespace is public
|
||||
parameters:
|
||||
- in: path
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Media list page rendered
|
||||
|
|
@ -274,47 +615,30 @@ paths:
|
|||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/media/user/{user_short_id}:
|
||||
get:
|
||||
summary: Display User's Media
|
||||
description: Shows all media uploaded by a specific user.
|
||||
parameters:
|
||||
- in: path
|
||||
name: user_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the user
|
||||
responses:
|
||||
'200':
|
||||
description: User's media page rendered
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
'403':
|
||||
description: Forbidden (insufficient permissions)
|
||||
'404':
|
||||
description: User not found
|
||||
'500':
|
||||
description: Internal Server Error
|
||||
description: Namespace not found
|
||||
|
||||
/media/{user_short_id}/{media_short_id}/details:
|
||||
/namespace/{namespace_short_id}/media/{media_short_id}/details:
|
||||
get:
|
||||
summary: Display Media Details
|
||||
description: Shows details of a specific media item.
|
||||
summary: View Media Details
|
||||
description: Displays the details of a media file.
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
- {} # Allow public access if media is public
|
||||
parameters:
|
||||
- in: path
|
||||
name: user_short_id
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the user
|
||||
- in: path
|
||||
name: media_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the media item
|
||||
responses:
|
||||
'200':
|
||||
description: Media details page rendered
|
||||
|
|
@ -322,30 +646,29 @@ paths:
|
|||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
'404':
|
||||
description: Media or user not found
|
||||
'403':
|
||||
description: Forbidden (media not public and not owner)
|
||||
description: Forbidden (insufficient permissions)
|
||||
'404':
|
||||
description: Media or namespace not found
|
||||
|
||||
/media/{user_short_id}/{media_short_id}/edit:
|
||||
/namespace/{namespace_short_id}/media/{media_short_id}/edit:
|
||||
get:
|
||||
summary: Display Media Edit Page
|
||||
description: Renders a form to edit media details (owner only).
|
||||
summary: Display Media Edit Page (Editors and Owners)
|
||||
description: Renders the media edit form. **Requires 'editor' or 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: user_short_id
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the user
|
||||
- in: path
|
||||
name: media_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the media item
|
||||
responses:
|
||||
'200':
|
||||
description: Media edit page rendered
|
||||
|
|
@ -354,27 +677,26 @@ paths:
|
|||
schema:
|
||||
type: string
|
||||
'403':
|
||||
description: Forbidden (not owner or not logged in)
|
||||
description: Forbidden (insufficient permissions)
|
||||
'404':
|
||||
description: Media not found
|
||||
description: Media or namespace not found
|
||||
post:
|
||||
summary: Edit Media
|
||||
description: Updates the media item (owner only).
|
||||
summary: Edit Media (Editors and Owners)
|
||||
description: Updates the media file or metadata. **Requires 'editor' or 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: user_short_id
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the user
|
||||
- in: path
|
||||
name: media_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the media item
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
|
@ -384,84 +706,103 @@ paths:
|
|||
properties:
|
||||
title:
|
||||
type: string
|
||||
is_public:
|
||||
type: string
|
||||
enum: ['on']
|
||||
media_file:
|
||||
type: string
|
||||
format: binary
|
||||
is_public:
|
||||
type: string
|
||||
enum: ['on']
|
||||
responses:
|
||||
'302':
|
||||
description: Redirects to the media details page
|
||||
headers:
|
||||
Location:
|
||||
description: URL of the media details page
|
||||
schema:
|
||||
type: string
|
||||
'400':
|
||||
description: Bad Request (e.g., file too large)
|
||||
description: Bad Request (e.g., invalid data)
|
||||
'403':
|
||||
description: Forbidden (not owner)
|
||||
description: Forbidden (insufficient permissions)
|
||||
'404':
|
||||
description: Media not found
|
||||
description: Media or namespace not found
|
||||
|
||||
/media/{user_short_id}/{media_short_id}/delete:
|
||||
/namespace/{namespace_short_id}/media/{media_short_id}/delete:
|
||||
post:
|
||||
summary: Delete Media
|
||||
description: Deletes the media item (owner only).
|
||||
summary: Delete Media (Editors and Owners)
|
||||
description: Deletes the specified media file. **Requires 'editor' or 'owner' role.**
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: user_short_id
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the user
|
||||
- in: path
|
||||
name: media_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the media item
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
csrf_token:
|
||||
type: string
|
||||
responses:
|
||||
'302':
|
||||
description: Redirects to the user's media list
|
||||
description: Redirects to the media list page
|
||||
headers:
|
||||
Location:
|
||||
description: URL of the media list page
|
||||
schema:
|
||||
type: string
|
||||
'403':
|
||||
description: Forbidden (not owner)
|
||||
description: Forbidden (insufficient permissions)
|
||||
'404':
|
||||
description: Media not found
|
||||
description: Media or namespace not found
|
||||
|
||||
/media/{user_short_id}/{media_short_id}:
|
||||
/namespace/{namespace_short_id}/media/{media_short_id}:
|
||||
get:
|
||||
summary: View Media
|
||||
description: Retrieves the media file for viewing or download.
|
||||
summary: View or Download Media
|
||||
description: Serves the media file for viewing or downloading.
|
||||
security:
|
||||
- sessionAuth: []
|
||||
- agentAuth: []
|
||||
- {} # Allow public access if media is public
|
||||
parameters:
|
||||
- in: path
|
||||
name: user_short_id
|
||||
name: namespace_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the user
|
||||
- in: path
|
||||
name: media_short_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The short ID of the media item
|
||||
- in: query
|
||||
name: download
|
||||
schema:
|
||||
type: string
|
||||
enum: ['true', 'false']
|
||||
description: Set to 'true' to trigger download
|
||||
type: boolean
|
||||
description: Set to true to download the file
|
||||
responses:
|
||||
'200':
|
||||
description: Media file retrieved
|
||||
description: Media file served
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
'403':
|
||||
description: Forbidden (media not public and not owner)
|
||||
description: Forbidden (insufficient permissions)
|
||||
'404':
|
||||
description: Media not found
|
||||
description: Media or namespace not found
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
|
|
@ -470,3 +811,8 @@ components:
|
|||
in: cookie
|
||||
name: session
|
||||
description: Session cookie for authenticated users
|
||||
agentAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: JWT authentication for agents
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ pyramid_retry
|
|||
pyramid_tm
|
||||
zope.sqlalchemy
|
||||
|
||||
pyramid_openapi3
|
||||
|
||||
bcrypt
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Media Hosting App{% endblock %}</title>
|
||||
<title>{% block title %}PyraFiles{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://www.unturf.com/css/pico.classless.min.css" rel="stylesheet">
|
||||
<style>
|
||||
|
|
@ -39,6 +39,26 @@
|
|||
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 */
|
||||
#mobile-menu-button {
|
||||
display: none;
|
||||
|
|
@ -99,16 +119,21 @@
|
|||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="{{ request.route_url('home') }}"><strong>upload.unturf.com</strong></a>
|
||||
<a href="{{ request.route_url('home') }}"><strong>{{ request.domain }}</strong></a>
|
||||
<!-- Hamburger Menu Button (only visible on mobile) -->
|
||||
<button id="mobile-menu-button" aria-label="Open Menu">☰</button>
|
||||
<!-- Navigation Links -->
|
||||
<ul>
|
||||
<li><a href="{{ request.route_url('list_media') }}">Browse Media</a></li>
|
||||
<li><a href="{{ request.route_url('home') }}">Home</a></li>
|
||||
{% if request.user and request.user.is_verified %}
|
||||
<li><a href="{{ request.route_url('upload_media') }}">Upload Media</a></li>
|
||||
<li><a href="{{ request.route_url('create_namespace') }}">Create Namespace</a></li>
|
||||
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
||||
<li><a href="{{ request.route_url('logout') }}">Logout</a></li>
|
||||
<li>
|
||||
<form action="{{ request.route_url('logout') }}" method="post" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
||||
<button type="submit" class="link-button">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
||||
{% endif %}
|
||||
|
|
@ -121,11 +146,16 @@
|
|||
<button aria-label="Close Menu" id="mobile-menu-close">✕</button>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ request.route_url('list_media') }}">Browse Media</a></li>
|
||||
<li><a href="{{ request.route_url('home') }}">Home</a></li>
|
||||
{% if request.user and request.user.is_verified %}
|
||||
<li><a href="{{ request.route_url('upload_media') }}">Upload Media</a></li>
|
||||
<li><a href="{{ request.route_url('create_namespace') }}">Create Namespace</a></li>
|
||||
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
||||
<li><a href="{{ request.route_url('logout') }}">Logout</a></li>
|
||||
<li>
|
||||
<form action="{{ request.route_url('logout') }}" method="post" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
||||
<button type="submit" class="link-button">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
||||
{% endif %}
|
||||
|
|
@ -134,14 +164,14 @@
|
|||
</article>
|
||||
</dialog>
|
||||
|
||||
<!-- Flash messages -->
|
||||
{% if request.session.peek_flash() %}
|
||||
{% for message in request.session.pop_flash() %}
|
||||
<div class="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<main class="container">
|
||||
<!-- Flash messages -->
|
||||
{% if request.session.peek_flash() %}
|
||||
{% for message in request.session.pop_flash() %}
|
||||
<div class="alert"><mark>{{ message }}</mark></div>
|
||||
<br/>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
|
|
|||
18
templates/create_namespace.html.j2
Normal file
18
templates/create_namespace.html.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{% 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 %}
|
||||
15
templates/display_agent_jwt.html.j2
Normal file
15
templates/display_agent_jwt.html.j2
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{% 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 interacting with 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 %}
|
||||
|
|
@ -3,13 +3,13 @@
|
|||
{% 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>
|
||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
||||
<label for="title">Title:</label>
|
||||
<input type="text" name="title" value="{{ media.title }}"><br><br>
|
||||
|
||||
<label for="media_file">Replace Media File (leave blank to keep current):</label>
|
||||
<label for="media_file">Replace Media File (optional):</label>
|
||||
<input type="file" name="media_file" accept="image/*,audio/*,video/*"><br><br>
|
||||
|
||||
<label>
|
||||
|
|
@ -17,19 +17,8 @@
|
|||
Make Media Public
|
||||
</label><br><br>
|
||||
|
||||
<button type="submit">Update</button>
|
||||
<button type="submit">Update Media</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>
|
||||
<p><a href="{{ request.route_url('view_media_details', namespace_short_id=namespace.short_id, media_short_id=media.short_id) }}">Back to Media Details</a></p>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Welcome to the Media Hosting App</h1>
|
||||
<h1>Welcome to PyraFiles</h1>
|
||||
<p>
|
||||
{% if request.user and request.user.is_verified %}
|
||||
Hello, {{ request.user.username }}!
|
||||
|
|
@ -11,5 +11,37 @@
|
|||
Welcome, Guest!
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>This application allows authenticated users to upload images, audio, and short videos. Guests can browse and download publicly available media.</p>
|
||||
<p>This application allows environments and agents to centrally manage and share files within namespaces. You can create namespaces to organize files and control access.</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('list_media', namespace_short_id=ns.short_id) }}">{{ ns.name }}</a> ({{ ns.role }})
|
||||
{% if ns.role == "owner" %}
|
||||
| <a href="{{ request.route_url('manage_namespace', namespace_short_id=ns.short_id) }}">Manage</a>
|
||||
{% endif %}
|
||||
</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('list_media', namespace_short_id=ns.short_id) }}">{{ ns.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No public namespaces available.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,25 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Media List{% endblock %}
|
||||
{% block title %}Media in {{ namespace.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Media List</h1>
|
||||
<h1>Media in Namespace: {{ namespace.name }}</h1>
|
||||
|
||||
{% if media_list %}
|
||||
{% if media_items %}
|
||||
<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 %}
|
||||
{% for media in media_items %}
|
||||
<li>
|
||||
<a href="{{ request.route_url('view_media_details', namespace_short_id=namespace.short_id, media_short_id=media.short_id) }}">{{ media.title or media.filename }}</a>
|
||||
({{ media.media_type }}, {{ media.size|filesizeformat }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No media found.</p>
|
||||
<p>No media found in this namespace.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user_namespace_role in ['owner', 'editor'] %}
|
||||
<p><a href="{{ request.route_url('upload_media', namespace_short_id=namespace.short_id) }}">Upload Media</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
107
templates/manage_namespace.html.j2
Normal file
107
templates/manage_namespace.html.j2
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
{% 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.role|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>
|
||||
|
||||
<label for="agent_role">Agent Role:</label>
|
||||
<select name="agent_role" required>
|
||||
<option value="owner">Owner</option>
|
||||
<option value="editor" selected>Editor</option>
|
||||
<option value="reader">Reader</option>
|
||||
</select><br><br>
|
||||
|
||||
<button type="submit">Generate JWT</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -23,15 +23,17 @@
|
|||
<button type="submit">Update Profile</button>
|
||||
</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>
|
||||
<h2>Your Owned Namespaces</h2>
|
||||
{% if owner_namespaces %}
|
||||
<ul>
|
||||
{% for ns in owner_namespaces %}
|
||||
<li>
|
||||
<a href="{{ request.route_url('manage_namespace', namespace_short_id=ns.short_id) }}">{{ ns.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>You do not own any namespaces.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
{% block title %}Upload Media{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Upload Media</h1>
|
||||
<h1>Upload Media to Namespace '{{ namespace.name }}'</h1>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<label for="media_file">Select Media File (Max 30MB):</label>
|
||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
||||
<label for="media_file">Media File:</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>
|
||||
<input type="text" name="title"><br><br>
|
||||
|
||||
<!-- Optionally, allow users to set media as public or private -->
|
||||
<label>
|
||||
<input type="checkbox" name="is_public" checked>
|
||||
Make Media Public
|
||||
|
|
|
|||
|
|
@ -4,21 +4,42 @@
|
|||
|
||||
{% block content %}
|
||||
<h1>Media Details</h1>
|
||||
|
||||
<p><strong>Title:</strong> {{ media.title if media.title else 'Untitled' }}</p>
|
||||
<p><strong>Title:</strong> {{ media.title or '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>
|
||||
<p><strong>Type:</strong> {{ media.media_type }}</p>
|
||||
<p><strong>Size:</strong> {{ media.size|filesizeformat }}</p>
|
||||
<p><strong>Uploaded:</strong> {{ media.upload_date.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
<p><strong>Public:</strong> {{ 'Yes' if media.is_public else 'No' }}</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>
|
||||
<h2>Preview</h2>
|
||||
{% if media.media_type == 'image' %}
|
||||
<img src="{{ request.route_url('view_media', namespace_short_id=namespace.short_id, media_short_id=media.short_id) }}" alt="{{ media.title }}" style="max-width: 100%;">
|
||||
{% elif media.media_type == 'audio' %}
|
||||
<audio controls>
|
||||
<source src="{{ request.route_url('view_media', namespace_short_id=namespace.short_id, media_short_id=media.short_id) }}" type="{{ media.media_type }}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
{% elif media.media_type == 'video' %}
|
||||
<video controls style="max-width: 100%;">
|
||||
<source src="{{ request.route_url('view_media', namespace_short_id=namespace.short_id, media_short_id=media.short_id) }}" type="{{ media.media_type }}">
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
{% else %}
|
||||
<p>No preview available.</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>Actions</h2>
|
||||
<p>
|
||||
<a href="{{ request.route_url('view_media', namespace_short_id=namespace.short_id, media_short_id=media.short_id) }}?download=true">Download</a>
|
||||
{% if is_owner_or_editor %}
|
||||
| <a href="{{ request.route_url('edit_media', namespace_short_id=namespace.short_id, media_short_id=media.short_id) }}">Edit</a>
|
||||
| <form method="POST" action="{{ request.route_url('delete_media', namespace_short_id=namespace.short_id, media_short_id=media.short_id) }}" class="inline-form" onsubmit="return confirm('Are you sure you want to delete this media?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}">
|
||||
<button type="submit" class="link-button">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<p><a href="{{ request.route_url('list_media', namespace_short_id=namespace.short_id) }}">Back to Media List</a></p>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue